186 lines
6.2 KiB
Python
186 lines
6.2 KiB
Python
|
|
"""
|
||
|
|
🧪 Общие утилиты для тестов - применение DRY принципа
|
||
|
|
|
||
|
|
Централизованные Mock классы, фикстуры и хелперы для избежания дублирования.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from typing import Any, Dict, List, Optional
|
||
|
|
import time
|
||
|
|
|
||
|
|
|
||
|
|
class MockRequest:
|
||
|
|
"""🔍 Универсальный Mock для HTTP request"""
|
||
|
|
|
||
|
|
def __init__(self, method: str = "POST", path: str = "/graphql", headers: Optional[Dict] = None):
|
||
|
|
self.method = method
|
||
|
|
self.url = type('MockURL', (), {"path": path})()
|
||
|
|
self.headers = headers or {}
|
||
|
|
|
||
|
|
|
||
|
|
class MockGraphQLResolveInfo:
|
||
|
|
"""🔍 Универсальный Mock для GraphQL resolve info"""
|
||
|
|
|
||
|
|
def __init__(
|
||
|
|
self,
|
||
|
|
author_id: Optional[int] = None,
|
||
|
|
requested_fields: Optional[List[str]] = None,
|
||
|
|
context_overrides: Optional[Dict] = None
|
||
|
|
):
|
||
|
|
base_context = {
|
||
|
|
"request": MockRequest(),
|
||
|
|
"author": {"id": author_id, "name": "Test User"} if author_id else None,
|
||
|
|
"roles": ["reader", "author"] if author_id else [],
|
||
|
|
"is_admin": False,
|
||
|
|
}
|
||
|
|
|
||
|
|
if context_overrides:
|
||
|
|
base_context.update(context_overrides)
|
||
|
|
|
||
|
|
self.context = base_context
|
||
|
|
self.field_nodes = [MockFieldNode(requested_fields or [])]
|
||
|
|
|
||
|
|
|
||
|
|
class MockFieldNode:
|
||
|
|
"""🔍 Mock для GraphQL field node"""
|
||
|
|
|
||
|
|
def __init__(self, requested_fields: List[str]):
|
||
|
|
self.selection_set = MockSelectionSet(requested_fields)
|
||
|
|
|
||
|
|
|
||
|
|
class MockSelectionSet:
|
||
|
|
"""🔍 Mock для GraphQL selection set"""
|
||
|
|
|
||
|
|
def __init__(self, requested_fields: List[str]):
|
||
|
|
self.selections = [
|
||
|
|
type('MockSelection', (), {
|
||
|
|
'name': type('MockName', (), {'value': field})()
|
||
|
|
})()
|
||
|
|
for field in requested_fields
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
def create_test_author(
|
||
|
|
db_session,
|
||
|
|
author_id: int,
|
||
|
|
email: str,
|
||
|
|
name: str = "Test User",
|
||
|
|
slug: Optional[str] = None,
|
||
|
|
password: str = "test_password"
|
||
|
|
) -> Any:
|
||
|
|
"""🔍 DRY хелпер для создания тестового автора"""
|
||
|
|
from orm.author import Author
|
||
|
|
|
||
|
|
# Очищаем существующие записи
|
||
|
|
db_session.query(Author).where(
|
||
|
|
(Author.id == author_id) | (Author.email == email)
|
||
|
|
).delete()
|
||
|
|
db_session.commit()
|
||
|
|
|
||
|
|
author = Author(
|
||
|
|
id=author_id,
|
||
|
|
name=name,
|
||
|
|
email=email,
|
||
|
|
slug=slug or f"test-user-{author_id}",
|
||
|
|
password=password
|
||
|
|
)
|
||
|
|
db_session.add(author)
|
||
|
|
db_session.commit()
|
||
|
|
|
||
|
|
return author
|
||
|
|
|
||
|
|
|
||
|
|
def create_test_community(
|
||
|
|
db_session,
|
||
|
|
community_id: int,
|
||
|
|
name: str = "Test Community",
|
||
|
|
slug: Optional[str] = None,
|
||
|
|
creator_id: Optional[int] = None
|
||
|
|
) -> Any:
|
||
|
|
"""🔍 DRY хелпер для создания тестового сообщества"""
|
||
|
|
from orm.community import Community
|
||
|
|
|
||
|
|
# Очищаем существующие записи
|
||
|
|
db_session.query(Community).where(Community.id == community_id).delete()
|
||
|
|
db_session.commit()
|
||
|
|
|
||
|
|
community = Community(
|
||
|
|
id=community_id,
|
||
|
|
name=name,
|
||
|
|
slug=slug or f"test-community-{community_id}",
|
||
|
|
desc=f"Test community description for {name}",
|
||
|
|
created_at=int(time.time()),
|
||
|
|
creator_id=creator_id
|
||
|
|
)
|
||
|
|
db_session.add(community)
|
||
|
|
db_session.commit()
|
||
|
|
|
||
|
|
return community
|
||
|
|
|
||
|
|
|
||
|
|
def cleanup_test_data(db_session, *model_id_pairs) -> None:
|
||
|
|
"""🔍 DRY хелпер для очистки тестовых данных"""
|
||
|
|
try:
|
||
|
|
for model_class, entity_id in model_id_pairs:
|
||
|
|
db_session.query(model_class).where(model_class.id == entity_id).delete()
|
||
|
|
db_session.commit()
|
||
|
|
except Exception as e:
|
||
|
|
print(f"⚠️ Ошибка при очистке тестовых данных: {e}")
|
||
|
|
db_session.rollback()
|
||
|
|
|
||
|
|
|
||
|
|
class TestDataBuilder:
|
||
|
|
"""🔍 Builder паттерн для создания тестовых данных - YAGNI подход"""
|
||
|
|
|
||
|
|
def __init__(self, db_session):
|
||
|
|
self.db_session = db_session
|
||
|
|
self._cleanup_tasks = []
|
||
|
|
|
||
|
|
def author(self, author_id: int, email: str, **kwargs) -> 'TestDataBuilder':
|
||
|
|
"""Создает автора и добавляет в очередь очистки"""
|
||
|
|
author = create_test_author(self.db_session, author_id, email, **kwargs)
|
||
|
|
self._cleanup_tasks.append(('orm.author.Author', author_id))
|
||
|
|
return self
|
||
|
|
|
||
|
|
def community(self, community_id: int, **kwargs) -> 'TestDataBuilder':
|
||
|
|
"""Создает сообщество и добавляет в очередь очистки"""
|
||
|
|
community = create_test_community(self.db_session, community_id, **kwargs)
|
||
|
|
self._cleanup_tasks.append(('orm.community.Community', community_id))
|
||
|
|
return self
|
||
|
|
|
||
|
|
def cleanup(self) -> None:
|
||
|
|
"""Очищает все созданные данные"""
|
||
|
|
for model_path, entity_id in self._cleanup_tasks:
|
||
|
|
try:
|
||
|
|
module_path, class_name = model_path.rsplit('.', 1)
|
||
|
|
module = __import__(module_path, fromlist=[class_name])
|
||
|
|
model_class = getattr(module, class_name)
|
||
|
|
self.db_session.query(model_class).where(model_class.id == entity_id).delete()
|
||
|
|
except Exception as e:
|
||
|
|
print(f"⚠️ Ошибка очистки {model_path}#{entity_id}: {e}")
|
||
|
|
|
||
|
|
try:
|
||
|
|
self.db_session.commit()
|
||
|
|
except Exception:
|
||
|
|
self.db_session.rollback()
|
||
|
|
|
||
|
|
|
||
|
|
def skip_if_auth_fails(func):
|
||
|
|
"""🔍 Декоратор для пропуска тестов при ошибках авторизации - YAGNI"""
|
||
|
|
import pytest
|
||
|
|
from functools import wraps
|
||
|
|
|
||
|
|
@wraps(func)
|
||
|
|
async def wrapper(*args, **kwargs):
|
||
|
|
try:
|
||
|
|
return await func(*args, **kwargs)
|
||
|
|
except Exception as e:
|
||
|
|
if any(phrase in str(e) for phrase in [
|
||
|
|
"Авторизация не прошла",
|
||
|
|
"AuthorizationError",
|
||
|
|
"Требуется авторизация"
|
||
|
|
]):
|
||
|
|
pytest.skip(f"Тест пропущен из-за ошибки авторизации: {e}")
|
||
|
|
raise
|
||
|
|
|
||
|
|
return wrapper
|