ci-testing
Some checks failed
Deploy on push / deploy (push) Failing after 1m11s

This commit is contained in:
2025-08-17 11:09:29 +03:00
parent 5876995838
commit 4b88a8c449
19 changed files with 2802 additions and 2559 deletions

View File

@@ -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