This commit is contained in:
@@ -8,6 +8,11 @@ import time
|
||||
import uuid
|
||||
from starlette.testclient import TestClient
|
||||
import requests
|
||||
import subprocess
|
||||
import signal
|
||||
import asyncio
|
||||
from typing import Optional, Generator, AsyncGenerator
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from services.redis import redis
|
||||
from orm.base import BaseModel as Base
|
||||
@@ -28,6 +33,8 @@ def get_test_client():
|
||||
return app
|
||||
|
||||
return TestClient(_import_app())
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, scope="session")
|
||||
def _set_requests_default_timeout():
|
||||
"""Глобально задаем таймаут по умолчанию для requests в тестах, чтобы исключить зависания.
|
||||
@@ -217,312 +224,357 @@ def db_session_commit(test_session_factory):
|
||||
session.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def frontend_url():
|
||||
"""
|
||||
Возвращает URL фронтенда для тестов.
|
||||
"""
|
||||
return FRONTEND_URL or "http://localhost:3000"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def backend_url():
|
||||
"""
|
||||
Возвращает URL бэкенда для тестов.
|
||||
"""
|
||||
return "http://localhost:8000"
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def test_app():
|
||||
"""Создает тестовое приложение"""
|
||||
from main import app
|
||||
return app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_client(test_app):
|
||||
"""Создает тестовый клиент"""
|
||||
from starlette.testclient import TestClient
|
||||
return TestClient(test_app)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def redis_client():
|
||||
"""Создает тестовый Redis клиент"""
|
||||
from services.redis import redis
|
||||
|
||||
# Очищаем тестовые данные
|
||||
await redis.execute("FLUSHDB")
|
||||
|
||||
yield redis
|
||||
|
||||
# Очищаем после тестов
|
||||
await redis.execute("FLUSHDB")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def oauth_db_session(test_session_factory):
|
||||
def backend_server():
|
||||
"""
|
||||
Создает сессию БД для OAuth тестов.
|
||||
🚀 Фикстура для автоматического запуска/остановки бэкенд сервера.
|
||||
Запускает сервер только если он не запущен.
|
||||
"""
|
||||
session = test_session_factory()
|
||||
yield session
|
||||
session.close()
|
||||
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__)))
|
||||
)
|
||||
|
||||
# Ждем запуска бэкенда
|
||||
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()
|
||||
|
||||
# ============================================================================
|
||||
# ОБЩИЕ ФИКСТУРЫ ДЛЯ RBAC ТЕСТОВ
|
||||
# ============================================================================
|
||||
|
||||
@pytest.fixture
|
||||
def unique_email():
|
||||
"""Генерирует уникальный email для каждого теста"""
|
||||
return f"test-{uuid.uuid4()}@example.com"
|
||||
def test_client(backend_server):
|
||||
"""
|
||||
🧪 Создает тестовый клиент для API тестов.
|
||||
Требует запущенный бэкенд сервер.
|
||||
"""
|
||||
return get_test_client()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
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()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
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()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def api_base_url(backend_server):
|
||||
"""
|
||||
🔗 Возвращает базовый URL для API тестов.
|
||||
"""
|
||||
return "http://localhost:8000/graphql"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_user_credentials():
|
||||
"""
|
||||
👤 Возвращает тестовые учетные данные для авторизации.
|
||||
"""
|
||||
return {
|
||||
"email": "test_admin@discours.io",
|
||||
"password": "password123"
|
||||
}
|
||||
|
||||
|
||||
@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
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
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
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_users(db_session):
|
||||
"""Создает тестовых пользователей для RBAC тестов"""
|
||||
from auth.orm import Author
|
||||
|
||||
users = []
|
||||
|
||||
# Создаем пользователей с ID 1-5
|
||||
for i in range(1, 6):
|
||||
user = db_session.query(Author).where(Author.id == i).first()
|
||||
if not user:
|
||||
user = Author(
|
||||
id=i,
|
||||
email=f"user{i}@example.com",
|
||||
name=f"Test User {i}",
|
||||
slug=f"test-user-{i}",
|
||||
created_at=int(time.time())
|
||||
)
|
||||
user.set_password("password123")
|
||||
db_session.add(user)
|
||||
users.append(user)
|
||||
|
||||
"""Создает тестовых пользователей для тестов"""
|
||||
from orm.community import Author
|
||||
|
||||
# Создаем первого пользователя (администратор)
|
||||
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={}
|
||||
)
|
||||
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)
|
||||
|
||||
db_session.commit()
|
||||
return users
|
||||
|
||||
return [admin_user, regular_user, reader_user]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_community(db_session, test_users):
|
||||
"""Создает тестовое сообщество для RBAC тестов"""
|
||||
"""Создает тестовое сообщество для тестов"""
|
||||
from orm.community import Community
|
||||
|
||||
community = db_session.query(Community).where(Community.id == 1).first()
|
||||
if not community:
|
||||
community = Community(
|
||||
id=1,
|
||||
name="Test Community",
|
||||
slug="test-community",
|
||||
desc="Test community for RBAC tests",
|
||||
created_by=test_users[0].id,
|
||||
created_at=int(time.time())
|
||||
)
|
||||
db_session.add(community)
|
||||
db_session.commit()
|
||||
|
||||
return community
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def simple_user(db_session):
|
||||
"""Создает простого тестового пользователя"""
|
||||
from auth.orm import Author
|
||||
from orm.community import CommunityAuthor
|
||||
|
||||
# Очищаем любые существующие записи с этим ID/email
|
||||
db_session.query(Author).where(
|
||||
(Author.id == 200) | (Author.email == "simple_user@example.com")
|
||||
).delete()
|
||||
db_session.commit()
|
||||
|
||||
user = Author(
|
||||
id=200,
|
||||
email="simple_user@example.com",
|
||||
name="Simple User",
|
||||
slug="simple-user",
|
||||
created_at=int(time.time())
|
||||
)
|
||||
user.set_password("password123")
|
||||
db_session.add(user)
|
||||
db_session.commit()
|
||||
|
||||
yield user
|
||||
|
||||
# Очистка после теста
|
||||
try:
|
||||
# Удаляем связанные записи CommunityAuthor
|
||||
db_session.query(CommunityAuthor).where(CommunityAuthor.author_id == user.id).delete(synchronize_session=False)
|
||||
# Удаляем самого пользователя
|
||||
db_session.query(Author).where(Author.id == user.id).delete()
|
||||
db_session.commit()
|
||||
except Exception:
|
||||
db_session.rollback()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def simple_community(db_session, simple_user):
|
||||
"""Создает простое тестовое сообщество"""
|
||||
from orm.community import Community, CommunityAuthor
|
||||
|
||||
# Очищаем любые существующие записи с этим ID/slug
|
||||
db_session.query(Community).where(Community.slug == "simple-test-community").delete()
|
||||
db_session.commit()
|
||||
|
||||
|
||||
community = Community(
|
||||
name="Simple Test Community",
|
||||
slug="simple-test-community",
|
||||
desc="Simple community for tests",
|
||||
created_by=simple_user.id,
|
||||
created_at=int(time.time()),
|
||||
name="Test Community",
|
||||
slug="test-community",
|
||||
desc="A test community for testing purposes",
|
||||
created_by=test_users[0].id, # Администратор создает сообщество
|
||||
settings={
|
||||
"default_roles": ["reader", "author"],
|
||||
"available_roles": ["reader", "author", "editor"]
|
||||
"custom_setting": "custom_value"
|
||||
}
|
||||
)
|
||||
db_session.add(community)
|
||||
db_session.commit()
|
||||
|
||||
return community
|
||||
|
||||
yield community
|
||||
|
||||
# Очистка после теста
|
||||
try:
|
||||
# Удаляем связанные записи CommunityAuthor
|
||||
db_session.query(CommunityAuthor).where(CommunityAuthor.community_id == community.id).delete()
|
||||
# Удаляем само сообщество
|
||||
db_session.query(Community).where(Community.id == community.id).delete()
|
||||
db_session.commit()
|
||||
except Exception:
|
||||
db_session.rollback()
|
||||
@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
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def community_without_creator(db_session):
|
||||
"""Создает сообщество без создателя (created_by = None)"""
|
||||
"""Создает сообщество без создателя"""
|
||||
from orm.community import Community
|
||||
|
||||
|
||||
community = Community(
|
||||
id=100,
|
||||
name="Community Without Creator",
|
||||
slug="community-without-creator",
|
||||
desc="Test community without creator",
|
||||
created_by=None, # Ключевое изменение - создатель отсутствует
|
||||
created_at=int(time.time())
|
||||
slug="community-without-creator",
|
||||
desc="A test community without a creator",
|
||||
created_by=None, # Без создателя
|
||||
settings={"default_roles": ["reader"]}
|
||||
)
|
||||
db_session.add(community)
|
||||
db_session.commit()
|
||||
|
||||
return community
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def admin_user_with_roles(db_session, test_users, test_community):
|
||||
"""Создает пользователя с ролями администратора"""
|
||||
"""Создает администратора с ролями в сообществе"""
|
||||
from orm.community import CommunityAuthor
|
||||
|
||||
user = test_users[0]
|
||||
|
||||
# Создаем CommunityAuthor с ролями администратора
|
||||
|
||||
ca = CommunityAuthor(
|
||||
community_id=test_community.id,
|
||||
author_id=user.id,
|
||||
roles="admin,editor,author"
|
||||
author_id=test_users[0].id,
|
||||
roles="admin,author,reader"
|
||||
)
|
||||
db_session.add(ca)
|
||||
db_session.commit()
|
||||
|
||||
return user
|
||||
|
||||
return test_users[0]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def regular_user_with_roles(db_session, test_users, test_community):
|
||||
"""Создает обычного пользователя с ролями"""
|
||||
"""Создает обычного пользователя с ролями в сообществе"""
|
||||
from orm.community import CommunityAuthor
|
||||
|
||||
user = test_users[1]
|
||||
|
||||
# Создаем CommunityAuthor с обычными ролями
|
||||
|
||||
ca = CommunityAuthor(
|
||||
community_id=test_community.id,
|
||||
author_id=user.id,
|
||||
roles="reader,author"
|
||||
author_id=test_users[1].id,
|
||||
roles="author,reader"
|
||||
)
|
||||
db_session.add(ca)
|
||||
db_session.commit()
|
||||
|
||||
return user
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# УТИЛИТЫ ДЛЯ ТЕСТОВ
|
||||
# ============================================================================
|
||||
|
||||
def create_test_user(db_session, user_id, email, name, slug, roles=None):
|
||||
"""Утилита для создания тестового пользователя с ролями"""
|
||||
from auth.orm import Author
|
||||
from orm.community import CommunityAuthor
|
||||
|
||||
# Создаем пользователя
|
||||
user = Author(
|
||||
id=user_id,
|
||||
email=email,
|
||||
name=name,
|
||||
slug=slug,
|
||||
created_at=int(time.time())
|
||||
)
|
||||
user.set_password("password123")
|
||||
db_session.add(user)
|
||||
db_session.commit()
|
||||
|
||||
# Добавляем роли если указаны
|
||||
if roles:
|
||||
ca = CommunityAuthor(
|
||||
community_id=1, # Используем основное сообщество
|
||||
author_id=user.id,
|
||||
roles=",".join(roles)
|
||||
)
|
||||
db_session.add(ca)
|
||||
db_session.commit()
|
||||
|
||||
return user
|
||||
|
||||
|
||||
def create_test_community(db_session, community_id, name, slug, created_by=None, settings=None):
|
||||
"""Утилита для создания тестового сообщества"""
|
||||
from orm.community import Community
|
||||
|
||||
community = Community(
|
||||
id=community_id,
|
||||
name=name,
|
||||
slug=slug,
|
||||
desc=f"Test community {name}",
|
||||
created_by=created_by,
|
||||
created_at=int(time.time()),
|
||||
settings=settings or {"default_roles": ["reader"], "available_roles": ["reader", "author", "editor", "admin"]}
|
||||
)
|
||||
db_session.add(community)
|
||||
db_session.commit()
|
||||
|
||||
return community
|
||||
|
||||
|
||||
def cleanup_test_data(db_session, user_ids=None, community_ids=None):
|
||||
"""Утилита для очистки тестовых данных"""
|
||||
from orm.community import CommunityAuthor
|
||||
|
||||
# Очищаем CommunityAuthor записи
|
||||
if user_ids:
|
||||
db_session.query(CommunityAuthor).where(CommunityAuthor.author_id.in_(user_ids)).delete(synchronize_session=False)
|
||||
|
||||
if community_ids:
|
||||
db_session.query(CommunityAuthor).where(CommunityAuthor.community_id.in_(community_ids)).delete(synchronize_session=False)
|
||||
|
||||
db_session.commit()
|
||||
|
||||
return test_users[1]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def frontend_url() -> str:
|
||||
"""URL фронтенда для тестов"""
|
||||
# В CI/CD используем порт 8000 (бэкенд), в локальной разработке - проверяем доступность фронтенда
|
||||
is_ci = os.getenv("PLAYWRIGHT_HEADLESS", "false").lower() == "true"
|
||||
if is_ci:
|
||||
return "http://localhost:8000"
|
||||
else:
|
||||
# Проверяем доступность фронтенда на порту 3000
|
||||
try:
|
||||
import requests
|
||||
response = requests.get("http://localhost:3000", timeout=2)
|
||||
if response.status_code == 200:
|
||||
return "http://localhost:3000"
|
||||
except:
|
||||
pass
|
||||
|
||||
# Если фронтенд недоступен, используем бэкенд на порту 8000
|
||||
return "http://localhost:8000"
|
||||
def mock_verify(monkeypatch):
|
||||
"""Мокает функцию верификации для тестов"""
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
mock = AsyncMock()
|
||||
# Здесь можно настроить возвращаемые значения по умолчанию
|
||||
return mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def redis_client():
|
||||
"""Создает Redis клиент для тестов токенов"""
|
||||
from services.redis import RedisService
|
||||
|
||||
redis_service = RedisService()
|
||||
return redis_service._client
|
||||
|
||||
Reference in New Issue
Block a user