6b69ec750e
- Add JWT authentication for cabinet users - Add Telegram WebApp authentication - Add subscription management endpoints - Add balance and transactions endpoints - Add referral system endpoints - Add tickets support for cabinet - Add webhooks and websocket for real-time updates - Add email verification service 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
220 lines
8.2 KiB
Python
220 lines
8.2 KiB
Python
"""Глобальные фикстуры и настройки окружения для тестов."""
|
|
|
|
import asyncio
|
|
import inspect
|
|
import os
|
|
import sys
|
|
import types
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
# Add project root to Python path for imports
|
|
project_root = Path(__file__).parent.parent
|
|
sys.path.insert(0, str(project_root))
|
|
|
|
# Подменяем параметры подключения к БД, чтобы SQLAlchemy не требовал aiosqlite.
|
|
os.environ.setdefault("DATABASE_MODE", "postgresql")
|
|
os.environ.setdefault("DATABASE_URL", "postgresql+asyncpg://user:pass@localhost/test_db")
|
|
os.environ.setdefault("BOT_TOKEN", "test-token")
|
|
|
|
# Создаём заглушки для драйверов, которых может не быть в окружении тестов.
|
|
sys.modules.setdefault("asyncpg", types.ModuleType("asyncpg"))
|
|
sys.modules.setdefault("aiosqlite", types.ModuleType("aiosqlite"))
|
|
|
|
# Эмуляция redis.asyncio, чтобы модуль кеша мог импортироваться.
|
|
if "redis.asyncio" not in sys.modules:
|
|
redis_module = types.ModuleType("redis")
|
|
redis_async_module = types.ModuleType("redis.asyncio")
|
|
|
|
class _FakeRedisClient:
|
|
async def ping(self):
|
|
"""Имитируем успешный ответ ping."""
|
|
return True
|
|
|
|
async def close(self):
|
|
"""Закрытие соединения ничего не делает."""
|
|
|
|
async def get(self, key): # noqa: ANN001
|
|
return None
|
|
|
|
async def set(self, key, value, ex=None): # noqa: ANN001
|
|
return True
|
|
|
|
async def delete(self, *keys): # noqa: ANN001
|
|
return 0
|
|
|
|
async def keys(self, pattern="*"): # noqa: ANN001
|
|
return []
|
|
|
|
async def exists(self, key): # noqa: ANN001
|
|
return False
|
|
|
|
async def expire(self, key, seconds): # noqa: ANN001
|
|
return True
|
|
|
|
async def incr(self, key): # noqa: ANN001
|
|
return 1
|
|
|
|
def _from_url(url): # noqa: ANN001
|
|
return _FakeRedisClient()
|
|
|
|
redis_async_module.from_url = _from_url
|
|
redis_async_module.Redis = _FakeRedisClient
|
|
sys.modules["redis"] = redis_module
|
|
sys.modules["redis.asyncio"] = redis_async_module
|
|
|
|
# Минимальная реализация SDK YooKassa, чтобы импорт сервисов не падал.
|
|
if "yookassa" not in sys.modules:
|
|
fake_yookassa = types.ModuleType("yookassa")
|
|
|
|
class _FakeConfiguration:
|
|
@staticmethod
|
|
def configure(*args, **kwargs):
|
|
"""Конфигурация заглушки ничего не делает."""
|
|
|
|
class _FakePayment:
|
|
@staticmethod
|
|
def create(*args, **kwargs):
|
|
"""Возвращает объект с минимально необходимыми атрибутами."""
|
|
|
|
class _Response:
|
|
id = "yk_fake"
|
|
status = "pending"
|
|
paid = False
|
|
refundable = False
|
|
metadata = {}
|
|
amount = types.SimpleNamespace(value="0.00", currency="RUB")
|
|
confirmation = types.SimpleNamespace(confirmation_url="https://example.com")
|
|
created_at = datetime.utcnow()
|
|
description = ""
|
|
test = False
|
|
|
|
return _Response()
|
|
|
|
fake_yookassa.Configuration = _FakeConfiguration
|
|
fake_yookassa.Payment = _FakePayment
|
|
sys.modules["yookassa"] = fake_yookassa
|
|
|
|
# Подготавливаем вложенные пакеты, используемые сервисом.
|
|
domain_module = types.ModuleType("yookassa.domain")
|
|
request_module = types.ModuleType("yookassa.domain.request")
|
|
payment_builder_module = types.ModuleType("yookassa.domain.request.payment_request_builder")
|
|
common_module = types.ModuleType("yookassa.domain.common")
|
|
confirmation_module = types.ModuleType("yookassa.domain.common.confirmation_type")
|
|
|
|
class _FakePaymentRequestBuilder:
|
|
def __init__(self):
|
|
self.data: dict = {}
|
|
|
|
def set_amount(self, value): # noqa: ANN001 - упрощённая заглушка
|
|
self.data["amount"] = value
|
|
return self
|
|
|
|
def set_capture(self, value): # noqa: ANN001
|
|
self.data["capture"] = value
|
|
return self
|
|
|
|
def set_confirmation(self, value): # noqa: ANN001
|
|
self.data["confirmation"] = value
|
|
return self
|
|
|
|
def set_description(self, value): # noqa: ANN001
|
|
self.data["description"] = value
|
|
return self
|
|
|
|
def set_metadata(self, value): # noqa: ANN001
|
|
self.data["metadata"] = value
|
|
return self
|
|
|
|
def set_receipt(self, value): # noqa: ANN001
|
|
self.data["receipt"] = value
|
|
return self
|
|
|
|
def set_payment_method_data(self, value): # noqa: ANN001
|
|
self.data["payment_method_data"] = value
|
|
return self
|
|
|
|
def build(self):
|
|
return self.data
|
|
|
|
class _FakeConfirmationType:
|
|
REDIRECT = "redirect"
|
|
|
|
payment_builder_module.PaymentRequestBuilder = _FakePaymentRequestBuilder
|
|
confirmation_module.ConfirmationType = _FakeConfirmationType
|
|
|
|
sys.modules["yookassa.domain"] = domain_module
|
|
sys.modules["yookassa.domain.request"] = request_module
|
|
sys.modules["yookassa.domain.request.payment_request_builder"] = payment_builder_module
|
|
sys.modules["yookassa.domain.common"] = common_module
|
|
sys.modules["yookassa.domain.common.confirmation_type"] = confirmation_module
|
|
|
|
|
|
@pytest.fixture
|
|
def fixed_datetime() -> datetime:
|
|
"""Возвращает фиксированную отметку времени для воспроизводимых проверок."""
|
|
return datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
|
|
|
|
|
|
def pytest_configure(config: pytest.Config) -> None:
|
|
"""Регистрируем маркеры для асинхронных тестов."""
|
|
|
|
config.addinivalue_line(
|
|
"markers",
|
|
"asyncio: запуск асинхронного теста через встроенный цикл событий",
|
|
)
|
|
config.addinivalue_line(
|
|
"markers",
|
|
"anyio: запуск асинхронного теста через встроенный цикл событий",
|
|
)
|
|
|
|
|
|
def _unwrap_test(obj): # noqa: ANN001 - вспомогательная функция для определения coroutine
|
|
"""Возвращает исходную функцию, снимая обёртки pytest и декораторов."""
|
|
|
|
unwrapped = obj
|
|
while hasattr(unwrapped, "__wrapped__"):
|
|
unwrapped = unwrapped.__wrapped__
|
|
return unwrapped
|
|
|
|
|
|
@pytest.hookimpl(tryfirst=True)
|
|
def pytest_pyfunc_call(pyfuncitem: pytest.Function) -> bool | None:
|
|
"""Позволяет запускать async def тесты без дополнительных плагинов."""
|
|
|
|
# Пропускаем если pytest-asyncio уже обработал этот тест
|
|
if hasattr(pyfuncitem, "_request") and hasattr(pyfuncitem._request, "_pyfuncitem"):
|
|
markers = list(pyfuncitem.iter_markers())
|
|
for marker in markers:
|
|
if marker.name in ("asyncio", "anyio"):
|
|
# pytest-asyncio обработает этот тест
|
|
return None
|
|
|
|
test_func = _unwrap_test(pyfuncitem.obj)
|
|
if not inspect.iscoroutinefunction(test_func):
|
|
return None
|
|
|
|
# Проверяем, не обработан ли уже тест плагином pytest-asyncio
|
|
# Если pyfuncitem.obj не возвращает корутину - пропускаем
|
|
loop = asyncio.new_event_loop()
|
|
try:
|
|
asyncio.set_event_loop(loop)
|
|
signature = inspect.signature(test_func)
|
|
call_kwargs = {
|
|
name: value
|
|
for name, value in pyfuncitem.funcargs.items()
|
|
if name in signature.parameters
|
|
}
|
|
coro = pyfuncitem.obj(**call_kwargs)
|
|
if coro is None:
|
|
# Уже обработано другим плагином
|
|
return None
|
|
loop.run_until_complete(coro)
|
|
finally:
|
|
asyncio.set_event_loop(None)
|
|
loop.close()
|
|
|
|
return True
|