Обновлен Python до версии 3.11.9 и изменены зависимости в Dockerfile и pyproject.toml. Удалены устаревшие файлы RATE_LIMITING_SOLUTION.md и тесты для rate limiting.
Обновлены пути к библиотекам в Dockerfile для соответствия новой версии Python. Исправлены все тесты, теперь все проходят
This commit is contained in:
88
.cursor/rules/architecture.md
Normal file
88
.cursor/rules/architecture.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
---
|
||||||
|
description: "Архитектурные паттерны и структура проекта Telegram бота на aiogram"
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Архитектура проекта
|
||||||
|
|
||||||
|
Этот проект - Telegram бот на **aiogram 3.10.0** с четкой архитектурой и разделением ответственности.
|
||||||
|
|
||||||
|
## Структура проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
helper_bot/
|
||||||
|
├── handlers/ # Обработчики событий (admin, callback, group, private, voice)
|
||||||
|
│ ├── services.py # Бизнес-логика для каждого модуля
|
||||||
|
│ ├── exceptions.py # Кастомные исключения модуля
|
||||||
|
│ └── dependencies.py # Dependency injection для модуля
|
||||||
|
├── middlewares/ # Middleware для cross-cutting concerns
|
||||||
|
├── utils/ # Утилиты и вспомогательные функции
|
||||||
|
├── keyboards/ # Клавиатуры для бота
|
||||||
|
└── filters/ # Кастомные фильтры
|
||||||
|
|
||||||
|
database/
|
||||||
|
├── repositories/ # Репозитории для работы с БД (Repository pattern)
|
||||||
|
├── models.py # Модели данных
|
||||||
|
├── base.py # Базовый класс DatabaseConnection
|
||||||
|
└── async_db.py # AsyncBotDB - основной интерфейс к БД
|
||||||
|
```
|
||||||
|
|
||||||
|
## Архитектурные паттерны
|
||||||
|
|
||||||
|
### 1. Repository Pattern
|
||||||
|
- Все операции с БД выполняются через репозитории в `database/repositories/`
|
||||||
|
- Каждая сущность имеет свой репозиторий (UserRepository, PostRepository, etc.)
|
||||||
|
- Репозитории наследуются от `DatabaseConnection` из `database/base.py`
|
||||||
|
- Используется `RepositoryFactory` для создания репозиториев
|
||||||
|
|
||||||
|
### 2. Service Layer Pattern
|
||||||
|
- Бизнес-логика вынесена в сервисы (`handlers/*/services.py`)
|
||||||
|
- Handlers только обрабатывают события и вызывают сервисы
|
||||||
|
- Сервисы работают с репозиториями через `AsyncBotDB`
|
||||||
|
|
||||||
|
### 3. Dependency Injection
|
||||||
|
- Используется `BaseDependencyFactory` для управления зависимостями
|
||||||
|
- Глобальный экземпляр доступен через `get_global_instance()`
|
||||||
|
- Зависимости внедряются через `DependenciesMiddleware`
|
||||||
|
- Для каждого модуля handlers может быть свой `dependencies.py` с фабриками
|
||||||
|
|
||||||
|
### 4. Middleware Pattern
|
||||||
|
- Middleware регистрируются в `main.py` на уровне dispatcher
|
||||||
|
- Порядок регистрации важен: DependenciesMiddleware → MetricsMiddleware → BlacklistMiddleware → RateLimitMiddleware
|
||||||
|
- Middleware обрабатывают cross-cutting concerns (логирование, метрики, rate limiting)
|
||||||
|
|
||||||
|
## Принципы
|
||||||
|
|
||||||
|
1. **Разделение ответственности**: Handlers → Services → Repositories
|
||||||
|
2. **Асинхронность**: Все операции с БД и API асинхронные
|
||||||
|
3. **Типизация**: Используются type hints везде, где возможно
|
||||||
|
4. **Логирование**: Всегда через `logs.custom_logger.logger`
|
||||||
|
5. **Метрики**: Декораторы `@track_time`, `@track_errors`, `@db_query_time` для мониторинга
|
||||||
|
|
||||||
|
## Версия Python
|
||||||
|
|
||||||
|
Проект использует **Python 3.11.9** во всех окружениях:
|
||||||
|
- Локальная разработка: Python 3.11.9 (указана в `.python-version`)
|
||||||
|
- Docker (production): Python 3.11.9-alpine (указана в `Dockerfile`)
|
||||||
|
- Минимальная версия: Python 3.11 (указана в `pyproject.toml`)
|
||||||
|
|
||||||
|
**Важно:**
|
||||||
|
- При написании кода можно использовать фичи Python 3.11
|
||||||
|
- Доступны улучшенные type hints, match/case (Python 3.10+)
|
||||||
|
- Используйте type hints везде, где возможно
|
||||||
|
- `@dataclass` доступен (Python 3.7+)
|
||||||
|
|
||||||
|
**Структура проекта:**
|
||||||
|
- Docker файлы находятся в двух местах:
|
||||||
|
- `/prod/Dockerfile` - для инфраструктуры (Python 3.11.9-alpine)
|
||||||
|
- `/prod/bots/telegram-helper-bot/Dockerfile` - для бота (Python 3.11.9-alpine)
|
||||||
|
- При обновлении версии Python нужно обновить оба Dockerfile
|
||||||
|
|
||||||
|
**Для локальной разработки:**
|
||||||
|
Рекомендуется использовать `pyenv` для установки Python 3.11.9:
|
||||||
|
```bash
|
||||||
|
pyenv install 3.11.9
|
||||||
|
pyenv local 3.11.9
|
||||||
|
```
|
||||||
|
|
||||||
|
Подробнее см. `docs/PYTHON_VERSION_MANAGEMENT.md`
|
||||||
106
.cursor/rules/code-style.md
Normal file
106
.cursor/rules/code-style.md
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
---
|
||||||
|
description: "Стиль кода, соглашения по именованию и форматированию"
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Стиль кода и соглашения
|
||||||
|
|
||||||
|
## Именование
|
||||||
|
|
||||||
|
### Классы
|
||||||
|
- **PascalCase**: `UserRepository`, `AdminService`, `BaseDependencyFactory`
|
||||||
|
- Имена классов должны быть существительными
|
||||||
|
|
||||||
|
### Функции и методы
|
||||||
|
- **snake_case**: `get_user_info()`, `handle_message()`, `create_tables()`
|
||||||
|
- Имена функций должны быть глаголами или начинаться с глагола
|
||||||
|
|
||||||
|
### Переменные
|
||||||
|
- **snake_case**: `user_id`, `bot_db`, `settings`, `message_text`
|
||||||
|
- Константы в **UPPER_SNAKE_CASE**: `FSM_STATES`, `ERROR_MESSAGES`
|
||||||
|
|
||||||
|
### Модули и пакеты
|
||||||
|
- **snake_case**: `admin_handlers.py`, `user_repository.py`
|
||||||
|
- Имена модулей должны быть короткими и понятными
|
||||||
|
|
||||||
|
## Импорты
|
||||||
|
|
||||||
|
Структура импортов (в порядке приоритета):
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 1. Standard library imports
|
||||||
|
import os
|
||||||
|
import asyncio
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
|
# 2. Third-party imports
|
||||||
|
from aiogram import Router, types
|
||||||
|
from aiogram.filters import Command
|
||||||
|
|
||||||
|
# 3. Local imports - модули проекта
|
||||||
|
from database.async_db import AsyncBotDB
|
||||||
|
from helper_bot.handlers.admin.services import AdminService
|
||||||
|
|
||||||
|
# 4. Local imports - utilities
|
||||||
|
from logs.custom_logger import logger
|
||||||
|
|
||||||
|
# 5. Local imports - metrics (если используются)
|
||||||
|
from helper_bot.utils.metrics import track_time, track_errors
|
||||||
|
```
|
||||||
|
|
||||||
|
## Type Hints
|
||||||
|
|
||||||
|
- Всегда используйте type hints для параметров функций и возвращаемых значений
|
||||||
|
- Используйте `Optional[T]` для значений, которые могут быть `None`
|
||||||
|
- Используйте `List[T]`, `Dict[K, V]` для коллекций
|
||||||
|
- Используйте `Annotated` для dependency injection в aiogram
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
```python
|
||||||
|
async def get_user_info(self, user_id: int) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Получение информации о пользователе."""
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Документация
|
||||||
|
|
||||||
|
### Docstrings
|
||||||
|
- Используйте docstrings для всех классов и публичных методов
|
||||||
|
- Формат: краткое описание в одну строку или многострочный с подробностями
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def add_user(self, user: User) -> None:
|
||||||
|
"""Добавление нового пользователя с защитой от дублирования."""
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Комментарии
|
||||||
|
- Комментарии на русском языке (как и весь код)
|
||||||
|
- Используйте комментарии для объяснения "почему", а не "что"
|
||||||
|
- Разделители секций: `# ============================================================================`
|
||||||
|
|
||||||
|
## Форматирование
|
||||||
|
|
||||||
|
- Используйте 4 пробела для отступов (не табы)
|
||||||
|
- Максимальная длина строки: 100-120 символов (гибко)
|
||||||
|
- Пустые строки между логическими блоками
|
||||||
|
- Пустая строка перед `return` в конце функции (если функция не короткая)
|
||||||
|
|
||||||
|
## Структура файлов handlers
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 1. Импорты (по категориям)
|
||||||
|
# 2. Создание роутера
|
||||||
|
router = Router()
|
||||||
|
|
||||||
|
# 3. Регистрация middleware (если нужно)
|
||||||
|
router.message.middleware(SomeMiddleware())
|
||||||
|
|
||||||
|
# 4. Handlers с декораторами
|
||||||
|
@router.message(...)
|
||||||
|
@track_time("handler_name", "module_name")
|
||||||
|
@track_errors("module_name", "handler_name")
|
||||||
|
async def handler_function(...):
|
||||||
|
"""Описание handler."""
|
||||||
|
...
|
||||||
|
```
|
||||||
117
.cursor/rules/database-patterns.md
Normal file
117
.cursor/rules/database-patterns.md
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
---
|
||||||
|
description: "Паттерны работы с базой данных, репозитории и модели"
|
||||||
|
globs: ["database/**/*.py", "**/repositories/*.py"]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Паттерны работы с базой данных
|
||||||
|
|
||||||
|
## Repository Pattern
|
||||||
|
|
||||||
|
Все операции с БД выполняются через репозитории. Каждый репозиторий:
|
||||||
|
- Наследуется от `DatabaseConnection` из `database/base.py`
|
||||||
|
- Работает с одной сущностью (User, Post, Blacklist, etc.)
|
||||||
|
- Содержит методы для CRUD операций
|
||||||
|
- Использует асинхронные методы `_execute_query()` и `_execute_query_with_result()`
|
||||||
|
|
||||||
|
### Структура репозитория
|
||||||
|
|
||||||
|
```python
|
||||||
|
from database.base import DatabaseConnection
|
||||||
|
from database.models import User
|
||||||
|
|
||||||
|
class UserRepository(DatabaseConnection):
|
||||||
|
"""Репозиторий для работы с пользователями."""
|
||||||
|
|
||||||
|
async def create_tables(self):
|
||||||
|
"""Создание таблицы пользователей."""
|
||||||
|
query = '''
|
||||||
|
CREATE TABLE IF NOT EXISTS our_users (
|
||||||
|
user_id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
...
|
||||||
|
)
|
||||||
|
'''
|
||||||
|
await self._execute_query(query)
|
||||||
|
self.logger.info("Таблица пользователей создана")
|
||||||
|
|
||||||
|
async def add_user(self, user: User) -> None:
|
||||||
|
"""Добавление нового пользователя."""
|
||||||
|
query = "INSERT OR IGNORE INTO our_users (...) VALUES (...)"
|
||||||
|
params = (...)
|
||||||
|
await self._execute_query(query, params)
|
||||||
|
self.logger.info(f"Пользователь добавлен: {user.user_id}")
|
||||||
|
|
||||||
|
async def get_user_by_id(self, user_id: int) -> Optional[User]:
|
||||||
|
"""Получение пользователя по ID."""
|
||||||
|
query = "SELECT * FROM our_users WHERE user_id = ?"
|
||||||
|
rows = await self._execute_query_with_result(query, (user_id,))
|
||||||
|
# Преобразование row в модель User
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Модели данных
|
||||||
|
|
||||||
|
- Модели определены в `database/models.py`
|
||||||
|
- Используются dataclasses или простые классы
|
||||||
|
- Модели передаются между слоями (Repository → Service → Handler)
|
||||||
|
|
||||||
|
## Работа с БД
|
||||||
|
|
||||||
|
### AsyncBotDB
|
||||||
|
- Основной интерфейс для работы с БД
|
||||||
|
- Использует `RepositoryFactory` для доступа к репозиториям
|
||||||
|
- Методы делегируют вызовы соответствующим репозиториям
|
||||||
|
|
||||||
|
### DatabaseConnection
|
||||||
|
- Базовый класс для всех репозиториев
|
||||||
|
- Предоставляет методы:
|
||||||
|
- `_get_connection()` - получение соединения
|
||||||
|
- `_execute_query()` - выполнение запроса без результата
|
||||||
|
- `_execute_query_with_result()` - выполнение запроса с результатом
|
||||||
|
- Автоматически управляет соединениями (открытие/закрытие)
|
||||||
|
- Настраивает PRAGMA для оптимизации SQLite
|
||||||
|
|
||||||
|
### Важные правила
|
||||||
|
|
||||||
|
1. **Всегда используйте параметризованные запросы** для защиты от SQL injection:
|
||||||
|
```python
|
||||||
|
# ✅ Правильно
|
||||||
|
query = "SELECT * FROM users WHERE user_id = ?"
|
||||||
|
await self._execute_query_with_result(query, (user_id,))
|
||||||
|
|
||||||
|
# ❌ Неправильно
|
||||||
|
query = f"SELECT * FROM users WHERE user_id = {user_id}"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Логируйте важные операции**:
|
||||||
|
```python
|
||||||
|
self.logger.info(f"Пользователь добавлен: {user_id}")
|
||||||
|
self.logger.error(f"Ошибка при добавлении пользователя: {e}")
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Используйте транзакции** для множественных операций (если нужно):
|
||||||
|
```python
|
||||||
|
async with await self._get_connection() as conn:
|
||||||
|
await conn.execute(...)
|
||||||
|
await conn.execute(...)
|
||||||
|
await conn.commit()
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Обрабатывайте None** при получении данных:
|
||||||
|
```python
|
||||||
|
rows = await self._execute_query_with_result(query, params)
|
||||||
|
if not rows:
|
||||||
|
return None
|
||||||
|
row = rows[0]
|
||||||
|
```
|
||||||
|
|
||||||
|
## RepositoryFactory
|
||||||
|
|
||||||
|
- Создает и кэширует экземпляры репозиториев
|
||||||
|
- Доступ через свойства: `factory.users`, `factory.posts`, etc.
|
||||||
|
- Используется в `AsyncBotDB` для доступа к репозиториям
|
||||||
|
|
||||||
|
## Миграции
|
||||||
|
|
||||||
|
- SQL миграции в `database/schema.sql`
|
||||||
|
- Python скрипты для миграций в `scripts/`
|
||||||
|
- Всегда проверяйте существование таблиц перед созданием: `CREATE TABLE IF NOT EXISTS`
|
||||||
172
.cursor/rules/dependencies-and-utils.md
Normal file
172
.cursor/rules/dependencies-and-utils.md
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
---
|
||||||
|
description: "Работа с зависимостями, утилитами, метриками и внешними сервисами"
|
||||||
|
globs: ["helper_bot/utils/**/*.py", "helper_bot/config/**/*.py"]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Зависимости и утилиты
|
||||||
|
|
||||||
|
## BaseDependencyFactory
|
||||||
|
|
||||||
|
Центральный класс для управления зависимостями проекта.
|
||||||
|
|
||||||
|
### Использование
|
||||||
|
|
||||||
|
```python
|
||||||
|
from helper_bot.utils.base_dependency_factory import get_global_instance
|
||||||
|
|
||||||
|
# Получение глобального экземпляра
|
||||||
|
bdf = get_global_instance()
|
||||||
|
|
||||||
|
# Доступ к зависимостям
|
||||||
|
db = bdf.get_db() # AsyncBotDB
|
||||||
|
settings = bdf.get_settings() # dict с настройками
|
||||||
|
s3_storage = bdf.get_s3_storage() # S3StorageService или None
|
||||||
|
```
|
||||||
|
|
||||||
|
### Структура settings
|
||||||
|
|
||||||
|
Настройки загружаются из `.env` и структурированы:
|
||||||
|
|
||||||
|
```python
|
||||||
|
settings = {
|
||||||
|
'Telegram': {
|
||||||
|
'bot_token': str,
|
||||||
|
'listen_bot_token': str,
|
||||||
|
'preview_link': bool,
|
||||||
|
'main_public': str,
|
||||||
|
'group_for_posts': int,
|
||||||
|
'important_logs': int,
|
||||||
|
...
|
||||||
|
},
|
||||||
|
'Settings': {
|
||||||
|
'logs': bool,
|
||||||
|
'test': bool
|
||||||
|
},
|
||||||
|
'Metrics': {
|
||||||
|
'host': str,
|
||||||
|
'port': int
|
||||||
|
},
|
||||||
|
'S3': {
|
||||||
|
'enabled': bool,
|
||||||
|
'endpoint_url': str,
|
||||||
|
'access_key': str,
|
||||||
|
'secret_key': str,
|
||||||
|
'bucket_name': str,
|
||||||
|
'region': str
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Метрики
|
||||||
|
|
||||||
|
### Декораторы метрик
|
||||||
|
|
||||||
|
Используйте декораторы из `helper_bot.utils.metrics`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from helper_bot.utils.metrics import track_time, track_errors, db_query_time
|
||||||
|
|
||||||
|
@track_time("method_name", "module_name")
|
||||||
|
@track_errors("module_name", "method_name")
|
||||||
|
async def some_method():
|
||||||
|
"""Метод с отслеживанием времени и ошибок."""
|
||||||
|
...
|
||||||
|
|
||||||
|
@db_query_time("method_name", "table_name", "operation")
|
||||||
|
async def db_method():
|
||||||
|
"""Метод БД с отслеживанием времени запросов."""
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Доступ к метрикам
|
||||||
|
|
||||||
|
```python
|
||||||
|
from helper_bot.utils.metrics import metrics
|
||||||
|
|
||||||
|
# Метрики доступны через Prometheus на порту из settings['Metrics']['port']
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
|
||||||
|
### RateLimiter
|
||||||
|
|
||||||
|
Используется для ограничения частоты запросов:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from helper_bot.utils.rate_limiter import RateLimiter
|
||||||
|
|
||||||
|
limiter = RateLimiter(...)
|
||||||
|
if await limiter.is_allowed(user_id):
|
||||||
|
# Разрешить действие
|
||||||
|
...
|
||||||
|
else:
|
||||||
|
# Отклонить действие
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### RateLimitMiddleware
|
||||||
|
|
||||||
|
Автоматически применяет rate limiting ко всем запросам через middleware.
|
||||||
|
|
||||||
|
## S3 Storage
|
||||||
|
|
||||||
|
### S3StorageService
|
||||||
|
|
||||||
|
Используется для хранения медиафайлов:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from helper_bot.utils.s3_storage import S3StorageService
|
||||||
|
|
||||||
|
# Получение через BaseDependencyFactory
|
||||||
|
s3_storage = bdf.get_s3_storage()
|
||||||
|
|
||||||
|
if s3_storage:
|
||||||
|
# Загрузка файла
|
||||||
|
url = await s3_storage.upload_file(file_path, object_key)
|
||||||
|
|
||||||
|
# Удаление файла
|
||||||
|
await s3_storage.delete_file(object_key)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проверка доступности
|
||||||
|
|
||||||
|
Всегда проверяйте, что S3 включен:
|
||||||
|
|
||||||
|
```python
|
||||||
|
s3_storage = bdf.get_s3_storage()
|
||||||
|
if s3_storage:
|
||||||
|
# Работа с S3
|
||||||
|
...
|
||||||
|
else:
|
||||||
|
# Fallback логика
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Утилиты
|
||||||
|
|
||||||
|
### helper_func.py
|
||||||
|
|
||||||
|
Содержит вспомогательные функции для работы с:
|
||||||
|
- Датами и временем
|
||||||
|
- Форматированием данных
|
||||||
|
- Валидацией
|
||||||
|
- Преобразованием данных
|
||||||
|
|
||||||
|
Используйте эти функции вместо дублирования логики.
|
||||||
|
|
||||||
|
## Конфигурация
|
||||||
|
|
||||||
|
### rate_limit_config.py
|
||||||
|
|
||||||
|
Конфигурация rate limiting находится в `helper_bot/config/rate_limit_config.py`.
|
||||||
|
|
||||||
|
Используйте конфигурацию вместо хардкода значений.
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Всегда получайте зависимости через BaseDependencyFactory** - не создавайте экземпляры напрямую
|
||||||
|
2. **Используйте декораторы метрик** для всех важных методов
|
||||||
|
3. **Проверяйте доступность внешних сервисов** (S3) перед использованием
|
||||||
|
4. **Используйте утилиты** из `helper_func.py` вместо дублирования кода
|
||||||
|
5. **Читайте настройки из settings** вместо хардкода значений
|
||||||
|
6. **Логируйте важные операции** с внешними сервисами
|
||||||
156
.cursor/rules/error-handling.md
Normal file
156
.cursor/rules/error-handling.md
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
---
|
||||||
|
description: "Обработка ошибок, исключения и логирование"
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Обработка ошибок и исключений
|
||||||
|
|
||||||
|
## Иерархия исключений
|
||||||
|
|
||||||
|
Каждый модуль должен иметь свой файл `exceptions.py` с иерархией исключений:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Базовое исключение модуля
|
||||||
|
class ModuleError(Exception):
|
||||||
|
"""Базовое исключение для модуля"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Специализированные исключения
|
||||||
|
class UserNotFoundError(ModuleError):
|
||||||
|
"""Исключение при отсутствии пользователя"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class InvalidInputError(ModuleError):
|
||||||
|
"""Исключение при некорректном вводе данных"""
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Обработка в Handlers
|
||||||
|
|
||||||
|
### Паттерн try-except
|
||||||
|
|
||||||
|
```python
|
||||||
|
@router.message(...)
|
||||||
|
async def handler(message: types.Message, state: FSMContext, **kwargs):
|
||||||
|
try:
|
||||||
|
# Основная логика
|
||||||
|
service = SomeService(bot_db, settings)
|
||||||
|
result = await service.do_something()
|
||||||
|
await message.answer("Успех")
|
||||||
|
except UserNotFoundError as e:
|
||||||
|
# Обработка специфичной ошибки
|
||||||
|
await message.answer(f"Пользователь не найден: {str(e)}")
|
||||||
|
logger.warning(f"Пользователь не найден: {e}")
|
||||||
|
except InvalidInputError as e:
|
||||||
|
# Обработка ошибки валидации
|
||||||
|
await message.answer(f"Некорректный ввод: {str(e)}")
|
||||||
|
logger.warning(f"Некорректный ввод: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
# Обработка неожиданных ошибок
|
||||||
|
await handle_error(message, e, state)
|
||||||
|
logger.error(f"Неожиданная ошибка в handler: {e}", exc_info=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Декоратор error_handler
|
||||||
|
|
||||||
|
Некоторые модули используют декоратор `@error_handler` для автоматической обработки:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from .decorators import error_handler
|
||||||
|
|
||||||
|
@error_handler
|
||||||
|
@router.message(...)
|
||||||
|
async def handler(...):
|
||||||
|
# Код handler
|
||||||
|
# Ошибки автоматически логируются и отправляются в important_logs
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Обработка в Services
|
||||||
|
|
||||||
|
```python
|
||||||
|
class SomeService:
|
||||||
|
async def do_something(self):
|
||||||
|
try:
|
||||||
|
# Бизнес-логика
|
||||||
|
data = await self.bot_db.get_data()
|
||||||
|
if not data:
|
||||||
|
raise UserNotFoundError("Пользователь не найден")
|
||||||
|
return self._process(data)
|
||||||
|
except UserNotFoundError:
|
||||||
|
# Пробрасываем специфичные исключения дальше
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
# Логируем и пробрасываем неожиданные ошибки
|
||||||
|
logger.error(f"Ошибка в сервисе: {e}", exc_info=True)
|
||||||
|
raise
|
||||||
|
```
|
||||||
|
|
||||||
|
## Логирование
|
||||||
|
|
||||||
|
### Использование logger
|
||||||
|
|
||||||
|
Всегда используйте `logs.custom_logger.logger`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from logs.custom_logger import logger
|
||||||
|
|
||||||
|
# Информационные сообщения
|
||||||
|
logger.info(f"Пользователь {user_id} выполнил действие")
|
||||||
|
|
||||||
|
# Предупреждения
|
||||||
|
logger.warning(f"Попытка доступа к несуществующему ресурсу: {resource_id}")
|
||||||
|
|
||||||
|
# Ошибки
|
||||||
|
logger.error(f"Ошибка при выполнении операции: {e}")
|
||||||
|
|
||||||
|
# Ошибки с traceback
|
||||||
|
logger.error(f"Критическая ошибка: {e}", exc_info=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Уровни логирования
|
||||||
|
|
||||||
|
- `logger.debug()` - отладочная информация
|
||||||
|
- `logger.info()` - информационные сообщения о работе
|
||||||
|
- `logger.warning()` - предупреждения о потенциальных проблемах
|
||||||
|
- `logger.error()` - ошибки, требующие внимания
|
||||||
|
- `logger.critical()` - критические ошибки
|
||||||
|
|
||||||
|
## Метрики ошибок
|
||||||
|
|
||||||
|
Декоратор `@track_errors` автоматически отслеживает ошибки:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@track_errors("module_name", "method_name")
|
||||||
|
async def some_method():
|
||||||
|
# Ошибки автоматически записываются в метрики
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Централизованная обработка
|
||||||
|
|
||||||
|
### В admin handlers
|
||||||
|
|
||||||
|
Используется функция `handle_admin_error()`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from helper_bot.handlers.admin.utils import handle_admin_error
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Код
|
||||||
|
except Exception as e:
|
||||||
|
await handle_admin_error(message, e, state, "context_name")
|
||||||
|
```
|
||||||
|
|
||||||
|
### В других модулях
|
||||||
|
|
||||||
|
Создавайте аналогичные утилиты для централизованной обработки ошибок модуля.
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Всегда логируйте ошибки** перед пробросом или обработкой
|
||||||
|
2. **Используйте специфичные исключения** вместо общих `Exception`
|
||||||
|
3. **Пробрасывайте исключения** из сервисов в handlers для обработки
|
||||||
|
4. **Не глотайте исключения** без логирования
|
||||||
|
5. **Используйте `exc_info=True`** для логирования traceback критических ошибок
|
||||||
|
6. **Обрабатывайте ошибки на правильном уровне**: бизнес-логика в сервисах, пользовательские сообщения в handlers
|
||||||
215
.cursor/rules/handlers-patterns.md
Normal file
215
.cursor/rules/handlers-patterns.md
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
---
|
||||||
|
description: "Паттерны для создания handlers, services и обработки событий aiogram"
|
||||||
|
globs: ["helper_bot/handlers/**/*.py"]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Паттерны для Handlers
|
||||||
|
|
||||||
|
## Структура модуля handler
|
||||||
|
|
||||||
|
Каждый модуль handler (admin, callback, group, private, voice) должен содержать:
|
||||||
|
|
||||||
|
```
|
||||||
|
handlers/{module}/
|
||||||
|
├── __init__.py # Экспорт router
|
||||||
|
├── {module}_handlers.py # Основные handlers
|
||||||
|
├── services.py # Бизнес-логика
|
||||||
|
├── exceptions.py # Кастомные исключения
|
||||||
|
├── dependencies.py # Dependency injection (опционально)
|
||||||
|
├── constants.py # Константы (FSM states, messages)
|
||||||
|
└── utils.py # Вспомогательные функции (опционально)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Создание Router
|
||||||
|
|
||||||
|
```python
|
||||||
|
from aiogram import Router
|
||||||
|
|
||||||
|
# Создаем роутер
|
||||||
|
router = Router()
|
||||||
|
|
||||||
|
# Регистрируем middleware (если нужно)
|
||||||
|
router.message.middleware(SomeMiddleware())
|
||||||
|
|
||||||
|
# Экспортируем в __init__.py
|
||||||
|
# from .{module}_handlers import router
|
||||||
|
```
|
||||||
|
|
||||||
|
## Структура Handler
|
||||||
|
|
||||||
|
```python
|
||||||
|
from aiogram import Router, types
|
||||||
|
from aiogram.filters import Command, StateFilter
|
||||||
|
from aiogram.fsm.context import FSMContext
|
||||||
|
from helper_bot.filters.main import ChatTypeFilter
|
||||||
|
from logs.custom_logger import logger
|
||||||
|
from helper_bot.utils.metrics import track_time, track_errors
|
||||||
|
|
||||||
|
router = Router()
|
||||||
|
|
||||||
|
@router.message(
|
||||||
|
ChatTypeFilter(chat_type=["private"]),
|
||||||
|
Command('command_name')
|
||||||
|
)
|
||||||
|
@track_time("handler_name", "module_name")
|
||||||
|
@track_errors("module_name", "handler_name")
|
||||||
|
async def handler_function(
|
||||||
|
message: types.Message,
|
||||||
|
state: FSMContext,
|
||||||
|
bot_db: AsyncBotDB, # Из DependenciesMiddleware
|
||||||
|
settings: dict, # Из DependenciesMiddleware
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
"""Описание handler."""
|
||||||
|
try:
|
||||||
|
# Логирование
|
||||||
|
logger.info(f"Обработка команды от пользователя: {message.from_user.id}")
|
||||||
|
|
||||||
|
# Получение данных из state (если нужно)
|
||||||
|
data = await state.get_data()
|
||||||
|
|
||||||
|
# Вызов сервиса для бизнес-логики
|
||||||
|
service = SomeService(bot_db, settings)
|
||||||
|
result = await service.do_something()
|
||||||
|
|
||||||
|
# Ответ пользователю
|
||||||
|
await message.answer("Результат")
|
||||||
|
|
||||||
|
# Обновление state (если нужно)
|
||||||
|
await state.set_state("NEW_STATE")
|
||||||
|
|
||||||
|
except SomeCustomException as e:
|
||||||
|
# Обработка кастомных исключений
|
||||||
|
await message.answer(f"Ошибка: {str(e)}")
|
||||||
|
logger.error(f"Ошибка в handler: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
# Обработка общих ошибок
|
||||||
|
await handle_error(message, e, state)
|
||||||
|
logger.error(f"Неожиданная ошибка: {e}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Service Layer
|
||||||
|
|
||||||
|
Бизнес-логика выносится в сервисы:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from logs.custom_logger import logger
|
||||||
|
from helper_bot.utils.metrics import track_time, track_errors
|
||||||
|
|
||||||
|
class SomeService:
|
||||||
|
"""Сервис для работы с ..."""
|
||||||
|
|
||||||
|
def __init__(self, bot_db: AsyncBotDB, settings: dict):
|
||||||
|
self.bot_db = bot_db
|
||||||
|
self.settings = settings
|
||||||
|
|
||||||
|
@track_time("method_name", "service_name")
|
||||||
|
@track_errors("service_name", "method_name")
|
||||||
|
async def do_something(self) -> SomeResult:
|
||||||
|
"""Описание метода."""
|
||||||
|
try:
|
||||||
|
# Работа с БД через bot_db
|
||||||
|
data = await self.bot_db.some_method()
|
||||||
|
|
||||||
|
# Бизнес-логика
|
||||||
|
result = self._process_data(data)
|
||||||
|
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка в сервисе: {e}")
|
||||||
|
raise
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependency Injection
|
||||||
|
|
||||||
|
### Через DependenciesMiddleware (глобально)
|
||||||
|
- `bot_db: AsyncBotDB` - доступен во всех handlers
|
||||||
|
- `settings: dict` - настройки из .env
|
||||||
|
- `bot: Bot` - экземпляр бота
|
||||||
|
- `dp: Dispatcher` - dispatcher
|
||||||
|
|
||||||
|
### Через MagicData (локально)
|
||||||
|
```python
|
||||||
|
from aiogram.filters import MagicData
|
||||||
|
from typing import Annotated
|
||||||
|
from helper_bot.handlers.admin.dependencies import BotDB, Settings
|
||||||
|
|
||||||
|
@router.message(
|
||||||
|
Command('admin'),
|
||||||
|
MagicData(bot_db=BotDB, settings=Settings)
|
||||||
|
)
|
||||||
|
async def handler(
|
||||||
|
message: types.Message,
|
||||||
|
bot_db: Annotated[AsyncBotDB, BotDB],
|
||||||
|
settings: Annotated[dict, Settings]
|
||||||
|
):
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Через фабрики (для сервисов)
|
||||||
|
```python
|
||||||
|
# В dependencies.py
|
||||||
|
def get_some_service() -> SomeService:
|
||||||
|
"""Фабрика для SomeService"""
|
||||||
|
bdf = get_global_instance()
|
||||||
|
db = bdf.get_db()
|
||||||
|
settings = bdf.settings
|
||||||
|
return SomeService(db, settings)
|
||||||
|
|
||||||
|
# В handlers
|
||||||
|
@router.message(Command('cmd'))
|
||||||
|
async def handler(
|
||||||
|
message: types.Message,
|
||||||
|
service: Annotated[SomeService, get_some_service()]
|
||||||
|
):
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## FSM (Finite State Machine)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Определение состояний в constants.py
|
||||||
|
FSM_STATES = {
|
||||||
|
"ADMIN": "ADMIN",
|
||||||
|
"AWAIT_INPUT": "AWAIT_INPUT",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
# Установка состояния
|
||||||
|
await state.set_state(FSM_STATES["ADMIN"])
|
||||||
|
|
||||||
|
# Получение состояния
|
||||||
|
current_state = await state.get_state()
|
||||||
|
|
||||||
|
# Сохранение данных
|
||||||
|
await state.update_data(key=value)
|
||||||
|
|
||||||
|
# Получение данных
|
||||||
|
data = await state.get_data()
|
||||||
|
value = data.get("key")
|
||||||
|
|
||||||
|
# Очистка состояния
|
||||||
|
await state.clear()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Фильтры
|
||||||
|
|
||||||
|
Используйте кастомные фильтры из `helper_bot.filters.main`:
|
||||||
|
- `ChatTypeFilter` - фильтр по типу чата (private, group, supergroup)
|
||||||
|
|
||||||
|
## Декораторы для метрик
|
||||||
|
|
||||||
|
Всегда добавляйте декораторы метрик к handlers и методам сервисов:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@track_time("handler_name", "module_name") # Измерение времени выполнения
|
||||||
|
@track_errors("module_name", "handler_name") # Отслеживание ошибок
|
||||||
|
@db_query_time("method_name", "table_name", "operation") # Для БД операций
|
||||||
|
```
|
||||||
|
|
||||||
|
## Обработка ошибок
|
||||||
|
|
||||||
|
- Используйте кастомные исключения из `exceptions.py`
|
||||||
|
- Обрабатывайте исключения в handlers
|
||||||
|
- Логируйте все ошибки через `logger.error()`
|
||||||
|
- Используйте декоратор `@error_handler` для автоматической обработки (если есть)
|
||||||
109
.cursor/rules/middleware-patterns.md
Normal file
109
.cursor/rules/middleware-patterns.md
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
---
|
||||||
|
description: "Паттерны создания и использования middleware в aiogram"
|
||||||
|
globs: ["helper_bot/middlewares/**/*.py"]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Паттерны Middleware
|
||||||
|
|
||||||
|
## Структура Middleware
|
||||||
|
|
||||||
|
Все middleware наследуются от `aiogram.BaseMiddleware`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from typing import Any, Dict
|
||||||
|
from aiogram import BaseMiddleware
|
||||||
|
from aiogram.types import TelegramObject
|
||||||
|
|
||||||
|
class CustomMiddleware(BaseMiddleware):
|
||||||
|
"""Описание middleware."""
|
||||||
|
|
||||||
|
async def __call__(
|
||||||
|
self,
|
||||||
|
handler: Callable,
|
||||||
|
event: TelegramObject,
|
||||||
|
data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
# Логика до обработки handler
|
||||||
|
...
|
||||||
|
|
||||||
|
# Вызов следующего handler в цепочке
|
||||||
|
result = await handler(event, data)
|
||||||
|
|
||||||
|
# Логика после обработки handler
|
||||||
|
...
|
||||||
|
|
||||||
|
return result
|
||||||
|
```
|
||||||
|
|
||||||
|
## Порядок регистрации Middleware
|
||||||
|
|
||||||
|
В `main.py` middleware регистрируются в следующем порядке (важно!):
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 1. DependenciesMiddleware - внедрение зависимостей
|
||||||
|
dp.update.outer_middleware(DependenciesMiddleware())
|
||||||
|
|
||||||
|
# 2. MetricsMiddleware - сбор метрик
|
||||||
|
dp.update.outer_middleware(MetricsMiddleware())
|
||||||
|
|
||||||
|
# 3. BlacklistMiddleware - проверка черного списка
|
||||||
|
dp.update.outer_middleware(BlacklistMiddleware())
|
||||||
|
|
||||||
|
# 4. RateLimitMiddleware - ограничение частоты запросов
|
||||||
|
dp.update.outer_middleware(RateLimitMiddleware())
|
||||||
|
```
|
||||||
|
|
||||||
|
## DependenciesMiddleware
|
||||||
|
|
||||||
|
Внедряет глобальные зависимости во все handlers:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class DependenciesMiddleware(BaseMiddleware):
|
||||||
|
async def __call__(self, handler, event: TelegramObject, data: Dict[str, Any]) -> Any:
|
||||||
|
bdf = get_global_instance()
|
||||||
|
|
||||||
|
# Внедрение зависимостей
|
||||||
|
if 'bot_db' not in data:
|
||||||
|
data['bot_db'] = bdf.get_db()
|
||||||
|
if 'settings' not in data:
|
||||||
|
data['settings'] = bdf.settings
|
||||||
|
|
||||||
|
return await handler(event, data)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Обработка ошибок в Middleware
|
||||||
|
|
||||||
|
```python
|
||||||
|
class CustomMiddleware(BaseMiddleware):
|
||||||
|
async def __call__(self, handler, event, data):
|
||||||
|
try:
|
||||||
|
# Предобработка
|
||||||
|
...
|
||||||
|
result = await handler(event, data)
|
||||||
|
# Постобработка
|
||||||
|
...
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
# Обработка ошибок
|
||||||
|
logger.error(f"Ошибка в middleware: {e}")
|
||||||
|
# Решаем: пробрасывать дальше или обработать
|
||||||
|
raise
|
||||||
|
```
|
||||||
|
|
||||||
|
## Регистрация на уровне Router
|
||||||
|
|
||||||
|
Middleware можно регистрировать на уровне конкретного router:
|
||||||
|
|
||||||
|
```python
|
||||||
|
router = Router()
|
||||||
|
router.message.middleware(SomeMiddleware()) # Только для message handlers
|
||||||
|
router.callback_query.middleware(SomeMiddleware()) # Только для callback handlers
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Регистрируйте middleware в правильном порядке** - зависимости должны быть первыми
|
||||||
|
2. **Не изменяйте event** напрямую, используйте `data` для передачи информации
|
||||||
|
3. **Обрабатывайте ошибки** в middleware, но не глотайте их без логирования
|
||||||
|
4. **Используйте `outer_middleware`** для глобальной регистрации
|
||||||
|
5. **Используйте `router.middleware()`** для локальной регистрации на уровне модуля
|
||||||
197
.cursor/rules/testing.md
Normal file
197
.cursor/rules/testing.md
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
---
|
||||||
|
description: "Паттерны тестирования, структура тестов и использование pytest"
|
||||||
|
globs: ["tests/**/*.py", "test_*.py"]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Паттерны тестирования
|
||||||
|
|
||||||
|
## Структура тестов
|
||||||
|
|
||||||
|
Тесты находятся в директории `tests/` и используют pytest:
|
||||||
|
|
||||||
|
```
|
||||||
|
tests/
|
||||||
|
├── conftest.py # Общие фикстуры
|
||||||
|
├── conftest_*.py # Специализированные фикстуры
|
||||||
|
├── mocks.py # Моки и заглушки
|
||||||
|
└── test_*.py # Тестовые файлы
|
||||||
|
```
|
||||||
|
|
||||||
|
## Конфигурация pytest
|
||||||
|
|
||||||
|
Настройки в `pyproject.toml`:
|
||||||
|
- `asyncio-mode=auto` - автоматический режим для async тестов
|
||||||
|
- Маркеры: `asyncio`, `slow`, `integration`, `unit`
|
||||||
|
- Фильтрация предупреждений
|
||||||
|
|
||||||
|
## Структура теста
|
||||||
|
|
||||||
|
```python
|
||||||
|
import pytest
|
||||||
|
from database.async_db import AsyncBotDB
|
||||||
|
from database.repositories.user_repository import UserRepository
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_user_repository_add_user(db_path):
|
||||||
|
"""Тест добавления пользователя."""
|
||||||
|
# Arrange
|
||||||
|
repo = UserRepository(db_path)
|
||||||
|
user = User(user_id=123, full_name="Test User")
|
||||||
|
|
||||||
|
# Act
|
||||||
|
await repo.add_user(user)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
result = await repo.get_user_by_id(123)
|
||||||
|
assert result is not None
|
||||||
|
assert result.full_name == "Test User"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Фикстуры
|
||||||
|
|
||||||
|
### Общие фикстуры (conftest.py)
|
||||||
|
|
||||||
|
```python
|
||||||
|
import pytest
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def db_path():
|
||||||
|
"""Создает временный файл БД для тестов."""
|
||||||
|
fd, path = tempfile.mkstemp(suffix='.db')
|
||||||
|
os.close(fd)
|
||||||
|
yield path
|
||||||
|
os.unlink(path)
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def async_db(db_path):
|
||||||
|
"""Создает AsyncBotDB для тестов."""
|
||||||
|
db = AsyncBotDB(db_path)
|
||||||
|
await db.create_tables()
|
||||||
|
yield db
|
||||||
|
```
|
||||||
|
|
||||||
|
### Использование фикстур
|
||||||
|
|
||||||
|
```python
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_something(async_db):
|
||||||
|
# async_db уже инициализирован
|
||||||
|
result = await async_db.some_method()
|
||||||
|
assert result is not None
|
||||||
|
```
|
||||||
|
|
||||||
|
## Моки
|
||||||
|
|
||||||
|
Используйте `mocks.py` для общих моков:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from unittest.mock import AsyncMock, MagicMock
|
||||||
|
|
||||||
|
def mock_bot():
|
||||||
|
"""Создает мок бота."""
|
||||||
|
bot = MagicMock()
|
||||||
|
bot.send_message = AsyncMock()
|
||||||
|
return bot
|
||||||
|
```
|
||||||
|
|
||||||
|
## Маркеры
|
||||||
|
|
||||||
|
Используйте маркеры для категоризации тестов:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@pytest.mark.unit
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_unit_test():
|
||||||
|
"""Быстрый unit тест."""
|
||||||
|
...
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_integration_test():
|
||||||
|
"""Медленный integration тест."""
|
||||||
|
...
|
||||||
|
|
||||||
|
@pytest.mark.slow
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_slow_test():
|
||||||
|
"""Медленный тест."""
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Запуск с фильтрацией:
|
||||||
|
```bash
|
||||||
|
pytest -m "not slow" # Пропустить медленные тесты
|
||||||
|
pytest -m unit # Только unit тесты
|
||||||
|
```
|
||||||
|
|
||||||
|
## Тестирование Handlers
|
||||||
|
|
||||||
|
```python
|
||||||
|
from aiogram import Bot, Dispatcher
|
||||||
|
from aiogram.fsm.storage.memory import MemoryStorage
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_handler(mock_bot, mock_message):
|
||||||
|
"""Тест handler."""
|
||||||
|
# Arrange
|
||||||
|
dp = Dispatcher(storage=MemoryStorage())
|
||||||
|
# Регистрация handler
|
||||||
|
...
|
||||||
|
|
||||||
|
# Act
|
||||||
|
await dp.feed_update(mock_update)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
mock_bot.send_message.assert_called_once()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Тестирование Services
|
||||||
|
|
||||||
|
```python
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_service_method(async_db):
|
||||||
|
"""Тест метода сервиса."""
|
||||||
|
service = SomeService(async_db, {})
|
||||||
|
result = await service.do_something()
|
||||||
|
assert result is not None
|
||||||
|
```
|
||||||
|
|
||||||
|
## Тестирование Repositories
|
||||||
|
|
||||||
|
```python
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_repository_crud(db_path):
|
||||||
|
"""Тест CRUD операций репозитория."""
|
||||||
|
repo = SomeRepository(db_path)
|
||||||
|
await repo.create_tables()
|
||||||
|
|
||||||
|
# Create
|
||||||
|
entity = SomeEntity(...)
|
||||||
|
await repo.add(entity)
|
||||||
|
|
||||||
|
# Read
|
||||||
|
result = await repo.get_by_id(entity.id)
|
||||||
|
assert result is not None
|
||||||
|
|
||||||
|
# Update
|
||||||
|
entity.field = "new_value"
|
||||||
|
await repo.update(entity)
|
||||||
|
|
||||||
|
# Delete
|
||||||
|
await repo.delete(entity.id)
|
||||||
|
result = await repo.get_by_id(entity.id)
|
||||||
|
assert result is None
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Используйте фикстуры** для переиспользования setup/teardown
|
||||||
|
2. **Изолируйте тесты** - каждый тест должен быть независимым
|
||||||
|
3. **Используйте временные БД** для тестов репозиториев
|
||||||
|
4. **Мокируйте внешние зависимости** (API, файловая система)
|
||||||
|
5. **Пишите понятные имена тестов** - они должны описывать что тестируется
|
||||||
|
6. **Используйте Arrange-Act-Assert** паттерн
|
||||||
|
7. **Тестируйте граничные случаи** и ошибки
|
||||||
@@ -1 +1 @@
|
|||||||
3.9.6
|
3.11.9
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
###########################################
|
###########################################
|
||||||
# Этап 1: Сборщик (Builder)
|
# Этап 1: Сборщик (Builder)
|
||||||
###########################################
|
###########################################
|
||||||
FROM python:3.9-alpine as builder
|
FROM python:3.11.9-alpine as builder
|
||||||
|
|
||||||
# Устанавливаем инструменты для компиляции + linux-headers для psutil
|
# Устанавливаем инструменты для компиляции + linux-headers для psutil
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
@@ -21,7 +21,7 @@ RUN pip install --no-cache-dir --target /install -r requirements.txt
|
|||||||
###########################################
|
###########################################
|
||||||
# Этап 2: Финальный образ (Runtime)
|
# Этап 2: Финальный образ (Runtime)
|
||||||
###########################################
|
###########################################
|
||||||
FROM python:3.9-alpine as runtime
|
FROM python:3.11.9-alpine as runtime
|
||||||
|
|
||||||
# Минимальные рантайм-зависимости
|
# Минимальные рантайм-зависимости
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
@@ -34,7 +34,7 @@ RUN addgroup -g 1001 deploy && adduser -D -u 1001 -G deploy deploy
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Копируем зависимости
|
# Копируем зависимости
|
||||||
COPY --from=builder --chown=1001:1001 /install /usr/local/lib/python3.9/site-packages
|
COPY --from=builder --chown=1001:1001 /install /usr/local/lib/python3.11/site-packages
|
||||||
|
|
||||||
# Создаем структуру папок
|
# Создаем структуру папок
|
||||||
RUN mkdir -p database logs voice_users && \
|
RUN mkdir -p database logs voice_users && \
|
||||||
|
|||||||
@@ -1,171 +0,0 @@
|
|||||||
# Решение проблемы Flood Control в Telegram Bot
|
|
||||||
|
|
||||||
## Проблема
|
|
||||||
|
|
||||||
В логах бота наблюдались ошибки типа:
|
|
||||||
```
|
|
||||||
Flood control exceeded on method 'SendVoice' in chat 1322897572. Retry in 3 seconds.
|
|
||||||
```
|
|
||||||
|
|
||||||
Эти ошибки возникают при превышении лимитов Telegram Bot API:
|
|
||||||
- Не более 30 сообщений в секунду от одного бота глобально
|
|
||||||
- Не более 1 сообщения в секунду в один чат
|
|
||||||
- Дополнительные ограничения для разных типов сообщений
|
|
||||||
|
|
||||||
## Решение
|
|
||||||
|
|
||||||
Реализована комплексная система rate limiting, включающая:
|
|
||||||
|
|
||||||
### 1. Основные компоненты
|
|
||||||
|
|
||||||
#### `rate_limiter.py`
|
|
||||||
- **ChatRateLimiter**: Ограничивает скорость отправки сообщений для конкретного чата
|
|
||||||
- **GlobalRateLimiter**: Глобальные ограничения для всех чатов
|
|
||||||
- **RetryHandler**: Обработка повторных попыток с экспоненциальной задержкой
|
|
||||||
- **TelegramRateLimiter**: Основной класс, объединяющий все компоненты
|
|
||||||
|
|
||||||
#### `rate_limit_monitor.py`
|
|
||||||
- **RateLimitMonitor**: Мониторинг и статистика rate limiting
|
|
||||||
- Отслеживание успешных/неудачных запросов
|
|
||||||
- Анализ ошибок и производительности
|
|
||||||
- Статистика по чатам
|
|
||||||
|
|
||||||
#### `rate_limit_config.py`
|
|
||||||
- Конфигурации для разных окружений (development, production, strict)
|
|
||||||
- Адаптивные настройки на основе уровня ошибок
|
|
||||||
- Настройки для разных типов сообщений
|
|
||||||
|
|
||||||
#### `rate_limit_middleware.py`
|
|
||||||
- Middleware для автоматического применения rate limiting
|
|
||||||
- Перехват всех исходящих сообщений
|
|
||||||
- Прозрачная интеграция с существующим кодом
|
|
||||||
|
|
||||||
### 2. Ключевые особенности
|
|
||||||
|
|
||||||
#### Rate Limiting
|
|
||||||
- **Настраиваемая скорость**: 0.5 сообщений в секунду на чат (по умолчанию)
|
|
||||||
- **Burst protection**: Максимум 2 сообщения подряд
|
|
||||||
- **Глобальные ограничения**: 10 сообщений в секунду глобально
|
|
||||||
- **Адаптивные задержки**: Увеличение задержек при ошибках
|
|
||||||
|
|
||||||
#### Retry Mechanism
|
|
||||||
- **Экспоненциальная задержка**: Увеличение времени ожидания при повторных попытках
|
|
||||||
- **Максимальные ограничения**: Ограничение максимального времени ожидания
|
|
||||||
- **Умная обработка ошибок**: Разные стратегии для разных типов ошибок
|
|
||||||
|
|
||||||
#### Мониторинг
|
|
||||||
- **Детальная статистика**: Отслеживание всех запросов и ошибок
|
|
||||||
- **Анализ производительности**: Процент успеха, время ожидания, активность
|
|
||||||
- **Административные команды**: `/ratelimit_stats`, `/ratelimit_errors`, `/reset_ratelimit_stats`
|
|
||||||
|
|
||||||
### 3. Интеграция
|
|
||||||
|
|
||||||
#### Обновленные функции
|
|
||||||
```python
|
|
||||||
# helper_func.py
|
|
||||||
async def send_voice_message(chat_id, message, voice, markup=None):
|
|
||||||
from .rate_limiter import send_with_rate_limit
|
|
||||||
|
|
||||||
async def _send_voice():
|
|
||||||
if markup is None:
|
|
||||||
return await message.bot.send_voice(chat_id=chat_id, voice=voice)
|
|
||||||
else:
|
|
||||||
return await message.bot.send_voice(chat_id=chat_id, voice=voice, reply_markup=markup)
|
|
||||||
|
|
||||||
return await send_with_rate_limit(_send_voice, chat_id)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Middleware
|
|
||||||
```python
|
|
||||||
# voice_handler.py
|
|
||||||
from helper_bot.middlewares.rate_limit_middleware import MessageSendMiddleware
|
|
||||||
|
|
||||||
def _setup_middleware(self):
|
|
||||||
self.router.message.middleware(DependenciesMiddleware())
|
|
||||||
self.router.message.middleware(BlacklistMiddleware())
|
|
||||||
self.router.message.middleware(MessageSendMiddleware()) # Новый middleware
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Конфигурация
|
|
||||||
|
|
||||||
#### Production настройки (по умолчанию)
|
|
||||||
```python
|
|
||||||
PRODUCTION_CONFIG = RateLimitSettings(
|
|
||||||
messages_per_second=0.5, # 1 сообщение каждые 2 секунды
|
|
||||||
burst_limit=2, # Максимум 2 сообщения подряд
|
|
||||||
retry_after_multiplier=1.5,
|
|
||||||
max_retry_delay=30.0,
|
|
||||||
max_retries=3,
|
|
||||||
voice_message_delay=2.5, # Дополнительная задержка для голосовых
|
|
||||||
media_message_delay=2.0,
|
|
||||||
text_message_delay=1.5
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Адаптивная конфигурация
|
|
||||||
Система автоматически ужесточает ограничения при высоком уровне ошибок:
|
|
||||||
- При >10% ошибок: уменьшение скорости в 2 раза
|
|
||||||
- При <1% ошибок: увеличение скорости на 20%
|
|
||||||
|
|
||||||
### 5. Мониторинг и администрирование
|
|
||||||
|
|
||||||
#### Команды для администраторов
|
|
||||||
- `/ratelimit_stats` - Показать статистику rate limiting
|
|
||||||
- `/ratelimit_errors` - Показать недавние ошибки
|
|
||||||
- `/reset_ratelimit_stats` - Сбросить статистику
|
|
||||||
|
|
||||||
#### Пример вывода статистики
|
|
||||||
```
|
|
||||||
📊 Статистика Rate Limiting
|
|
||||||
|
|
||||||
🔢 Общая статистика:
|
|
||||||
• Всего запросов: 1250
|
|
||||||
• Процент успеха: 98.4%
|
|
||||||
• Процент ошибок: 1.6%
|
|
||||||
• Запросов в минуту: 12.5
|
|
||||||
• Среднее время ожидания: 1.2с
|
|
||||||
• Активных чатов: 45
|
|
||||||
• Ошибок за час: 3
|
|
||||||
|
|
||||||
🔍 Детальная статистика:
|
|
||||||
• Успешных запросов: 1230
|
|
||||||
• Неудачных запросов: 20
|
|
||||||
• RetryAfter ошибок: 15
|
|
||||||
• Других ошибок: 5
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. Тестирование
|
|
||||||
|
|
||||||
Создан полный набор тестов в `test_rate_limiter.py`:
|
|
||||||
- Тесты всех компонентов
|
|
||||||
- Интеграционные тесты
|
|
||||||
- Тесты конфигурации
|
|
||||||
- Тесты мониторинга
|
|
||||||
|
|
||||||
Запуск тестов:
|
|
||||||
```bash
|
|
||||||
pytest tests/test_rate_limiter.py -v
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7. Преимущества решения
|
|
||||||
|
|
||||||
1. **Предотвращение ошибок**: Автоматическое соблюдение лимитов API
|
|
||||||
2. **Прозрачность**: Минимальные изменения в существующем коде
|
|
||||||
3. **Мониторинг**: Полная видимость производительности
|
|
||||||
4. **Адаптивность**: Автоматическая настройка под нагрузку
|
|
||||||
5. **Надежность**: Умная обработка ошибок и повторных попыток
|
|
||||||
6. **Масштабируемость**: Поддержка множества чатов
|
|
||||||
|
|
||||||
### 8. Рекомендации по использованию
|
|
||||||
|
|
||||||
1. **Мониторинг**: Регулярно проверяйте статистику через `/ratelimit_stats`
|
|
||||||
2. **Настройка**: При необходимости корректируйте конфигурацию под ваши нужды
|
|
||||||
3. **Алерты**: Настройте уведомления при высоком проценте ошибок
|
|
||||||
4. **Тестирование**: Проверяйте работу в тестовой среде перед продакшеном
|
|
||||||
|
|
||||||
### 9. Будущие улучшения
|
|
||||||
|
|
||||||
- Интеграция с системой метрик (Prometheus/Grafana)
|
|
||||||
- Автоматическое масштабирование ограничений
|
|
||||||
- A/B тестирование разных конфигураций
|
|
||||||
- Интеграция с системой алертов
|
|
||||||
710
docs/IMPROVEMENTS.md
Normal file
710
docs/IMPROVEMENTS.md
Normal file
@@ -0,0 +1,710 @@
|
|||||||
|
# План улучшений проекта
|
||||||
|
|
||||||
|
Этот документ содержит список рекомендаций по улучшению кодовой базы проекта Telegram Helper Bot. Пункты отсортированы по приоритетам и могут быть использованы для планирования работ.
|
||||||
|
|
||||||
|
## Статус задач
|
||||||
|
|
||||||
|
- ⬜ Не начато
|
||||||
|
- 🟡 В работе
|
||||||
|
- ✅ Выполнено
|
||||||
|
- ❌ Отложено
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔴 Высокий приоритет
|
||||||
|
|
||||||
|
### 1. Стандартизация Dependency Injection
|
||||||
|
|
||||||
|
**Статус:** ⬜
|
||||||
|
|
||||||
|
**Проблема:**
|
||||||
|
В проекте используется смешанный подход к dependency injection:
|
||||||
|
- В некоторых местах используется `MagicData("bot_db")` и `MagicData("settings")`
|
||||||
|
- В других местах используется `**kwargs` и получение из `data`
|
||||||
|
- В сервисах напрямую вызывается `get_global_instance()`
|
||||||
|
|
||||||
|
**Текущее состояние:**
|
||||||
|
```python
|
||||||
|
# callback_handlers.py - смешанный подход
|
||||||
|
async def handler(call: CallbackQuery, settings: MagicData("settings")):
|
||||||
|
publish_service = get_post_publish_service() # Прямой вызов фабрики
|
||||||
|
|
||||||
|
async def handler(call: CallbackQuery, **kwargs):
|
||||||
|
ban_service = get_ban_service() # Прямой вызов фабрики
|
||||||
|
```
|
||||||
|
|
||||||
|
**Рекомендация:**
|
||||||
|
Стандартизировать на использование `MagicData` и `Annotated` везде:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from typing import Annotated
|
||||||
|
from aiogram.filters import MagicData
|
||||||
|
from helper_bot.handlers.admin.dependencies import BotDB, Settings
|
||||||
|
|
||||||
|
async def handler(
|
||||||
|
call: CallbackQuery,
|
||||||
|
bot_db: Annotated[AsyncBotDB, BotDB],
|
||||||
|
settings: Annotated[dict, Settings],
|
||||||
|
service: Annotated[PostPublishService, get_post_publish_service()]
|
||||||
|
):
|
||||||
|
# Использовать зависимости напрямую
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Файлы для изменения:**
|
||||||
|
- `helper_bot/handlers/callback/callback_handlers.py` (строки 47, 80, 109, 131, 182)
|
||||||
|
- `helper_bot/handlers/private/private_handlers.py`
|
||||||
|
- Все сервисы, которые используют `get_global_instance()`
|
||||||
|
|
||||||
|
**Оценка:** Средняя сложность, требует рефакторинга нескольких файлов
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Удаление `import *`
|
||||||
|
|
||||||
|
**Статус:** ⬜
|
||||||
|
|
||||||
|
**Проблема:**
|
||||||
|
В `voice_handler.py` используется импорт всех констант через `import *`, что затрудняет понимание зависимостей и может привести к конфликтам имен.
|
||||||
|
|
||||||
|
**Текущее состояние:**
|
||||||
|
```python
|
||||||
|
# helper_bot/handlers/voice/voice_handler.py
|
||||||
|
from helper_bot.handlers.voice.constants import *
|
||||||
|
```
|
||||||
|
|
||||||
|
**Рекомендация:**
|
||||||
|
Заменить на явные импорты:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from helper_bot.handlers.voice.constants import (
|
||||||
|
CONSTANT1,
|
||||||
|
CONSTANT2,
|
||||||
|
CONSTANT3,
|
||||||
|
# ... все используемые константы
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Файлы для изменения:**
|
||||||
|
- `helper_bot/handlers/voice/voice_handler.py` (строка 17)
|
||||||
|
|
||||||
|
**Оценка:** Низкая сложность, быстрое исправление
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Закрытие критичных TODO
|
||||||
|
|
||||||
|
**Статус:** ⬜
|
||||||
|
|
||||||
|
**Проблема:**
|
||||||
|
В коде есть несколько TODO комментариев, указывающих на технический долг и места, требующие рефакторинга.
|
||||||
|
|
||||||
|
**Список TODO:**
|
||||||
|
|
||||||
|
#### 3.1. Callback handlers - переход на MagicData
|
||||||
|
**Файл:** `helper_bot/handlers/callback/callback_handlers.py`
|
||||||
|
- Строка 47: `# TODO: переделать на MagicData`
|
||||||
|
- Строка 80: `# TODO: переделать на MagicData`
|
||||||
|
- Строка 109: `# TODO: переделать на MagicData`
|
||||||
|
- Строка 131: `# TODO: переделать на MagicData`
|
||||||
|
- Строка 182: `# TODO: переделать на MagicData`
|
||||||
|
|
||||||
|
**Решение:** Связано с задачей #1 (стандартизация DI)
|
||||||
|
|
||||||
|
#### 3.2. Metrics middleware - подключение к БД
|
||||||
|
**Файл:** `helper_bot/middlewares/metrics_middleware.py`
|
||||||
|
- Строка 153: `#TODO: Должна подключаться к базе данных, а не к глобальному экземпляру`
|
||||||
|
|
||||||
|
**Решение:**
|
||||||
|
```python
|
||||||
|
# Вместо
|
||||||
|
bdf = get_global_instance()
|
||||||
|
bot_db = bdf.get_db()
|
||||||
|
|
||||||
|
# Использовать dependency injection через MagicData
|
||||||
|
async def _update_active_users_metric(
|
||||||
|
self,
|
||||||
|
bot_db: Annotated[AsyncBotDB, BotDB]
|
||||||
|
):
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.3. Voice handler - вынос логики
|
||||||
|
**Файл:** `helper_bot/handlers/voice/voice_handler.py`
|
||||||
|
- Строка 354: `#TODO: удалить логику из хендлера`
|
||||||
|
|
||||||
|
**Решение:** Переместить бизнес-логику в `VoiceBotService`
|
||||||
|
|
||||||
|
#### 3.4. Helper functions - архитектура
|
||||||
|
**Файл:** `helper_bot/utils/helper_func.py`
|
||||||
|
- Строка 35: `#TODO: поменять архитектуру и подключить правильный BotDB`
|
||||||
|
- Строка 145: `#TODO: Уверен можно укоротить`
|
||||||
|
|
||||||
|
**Решение:** Рефакторинг функций для использования dependency injection
|
||||||
|
|
||||||
|
#### 3.5. Group handlers - архитектура
|
||||||
|
**Файл:** `helper_bot/handlers/group/group_handlers.py`
|
||||||
|
- Строка 109: `#TODO: поменять архитектуру и подключить правильный BotDB`
|
||||||
|
|
||||||
|
**Решение:** Использовать dependency injection вместо прямого доступа к БД
|
||||||
|
|
||||||
|
**Оценка:** Средняя-высокая сложность, требует анализа каждого случая
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟡 Средний приоритет
|
||||||
|
|
||||||
|
### 4. Оптимизация работы с БД - Connection Pooling
|
||||||
|
|
||||||
|
**Статус:** ⬜
|
||||||
|
|
||||||
|
**Проблема:**
|
||||||
|
Каждый запрос к БД открывает новое соединение и закрывает его. При высокой нагрузке это неэффективно и может привести к проблемам с производительностью.
|
||||||
|
|
||||||
|
**Текущее состояние:**
|
||||||
|
```python
|
||||||
|
# database/base.py
|
||||||
|
async def _get_connection(self):
|
||||||
|
conn = await aiosqlite.connect(self.db_path)
|
||||||
|
# Настройка PRAGMA каждый раз
|
||||||
|
await conn.execute("PRAGMA foreign_keys = ON")
|
||||||
|
await conn.execute("PRAGMA journal_mode = WAL")
|
||||||
|
# ...
|
||||||
|
return conn
|
||||||
|
|
||||||
|
async def _execute_query(self, query: str, params: tuple = ()):
|
||||||
|
conn = None
|
||||||
|
try:
|
||||||
|
conn = await self._get_connection() # Новое соединение каждый раз
|
||||||
|
result = await conn.execute(query, params)
|
||||||
|
await conn.commit()
|
||||||
|
return result
|
||||||
|
finally:
|
||||||
|
if conn:
|
||||||
|
await conn.close() # Закрытие после каждого запроса
|
||||||
|
```
|
||||||
|
|
||||||
|
**Рекомендация:**
|
||||||
|
Реализовать переиспользование соединений или connection pool:
|
||||||
|
|
||||||
|
**Вариант 1: Переиспользование соединения в рамках транзакции**
|
||||||
|
```python
|
||||||
|
class DatabaseConnection:
|
||||||
|
def __init__(self, db_path: str):
|
||||||
|
self.db_path = db_path
|
||||||
|
self._connection: Optional[aiosqlite.Connection] = None
|
||||||
|
|
||||||
|
async def _get_connection(self):
|
||||||
|
if self._connection is None:
|
||||||
|
self._connection = await aiosqlite.connect(self.db_path)
|
||||||
|
# Настройка PRAGMA один раз
|
||||||
|
await self._connection.execute("PRAGMA foreign_keys = ON")
|
||||||
|
# ...
|
||||||
|
return self._connection
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
if self._connection:
|
||||||
|
await self._connection.close()
|
||||||
|
self._connection = None
|
||||||
|
```
|
||||||
|
|
||||||
|
**Вариант 2: Использование async context manager**
|
||||||
|
```python
|
||||||
|
async def _execute_query(self, query: str, params: tuple = ()):
|
||||||
|
async with aiosqlite.connect(self.db_path) as conn:
|
||||||
|
await conn.execute("PRAGMA foreign_keys = ON")
|
||||||
|
result = await conn.execute(query, params)
|
||||||
|
await conn.commit()
|
||||||
|
return result
|
||||||
|
```
|
||||||
|
|
||||||
|
**Файлы для изменения:**
|
||||||
|
- `database/base.py`
|
||||||
|
- `database/repository_factory.py` (добавить метод `close()`)
|
||||||
|
- `helper_bot/utils/base_dependency_factory.py` (закрытие соединений при shutdown)
|
||||||
|
|
||||||
|
**Оценка:** Средняя сложность, требует тестирования на производительность
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Улучшение обработки ошибок - декораторы
|
||||||
|
|
||||||
|
**Статус:** ⬜
|
||||||
|
|
||||||
|
**Проблема:**
|
||||||
|
В `callback_handlers.py` повторяется один и тот же блок обработки ошибок в каждом handler:
|
||||||
|
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
# Бизнес-логика
|
||||||
|
except UserBlockedBotError:
|
||||||
|
await call.answer(text=MESSAGE_ERROR, show_alert=True, cache_time=3)
|
||||||
|
except (PostNotFoundError, PublishError) as e:
|
||||||
|
logger.error(f'Ошибка: {str(e)}')
|
||||||
|
await call.answer(text=MESSAGE_ERROR, show_alert=True, cache_time=3)
|
||||||
|
except Exception as e:
|
||||||
|
if str(e) == ERROR_BOT_BLOCKED:
|
||||||
|
await call.answer(text=MESSAGE_ERROR, show_alert=True, cache_time=3)
|
||||||
|
else:
|
||||||
|
important_logs = settings['Telegram']['important_logs']
|
||||||
|
await call.bot.send_message(
|
||||||
|
chat_id=important_logs,
|
||||||
|
text=f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
||||||
|
)
|
||||||
|
logger.error(f'Неожиданная ошибка: {str(e)}')
|
||||||
|
await call.answer(text=MESSAGE_ERROR, show_alert=True, cache_time=3)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Рекомендация:**
|
||||||
|
Создать декоратор для централизованной обработки ошибок:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# helper_bot/handlers/callback/decorators.py
|
||||||
|
from functools import wraps
|
||||||
|
from typing import Callable, Any
|
||||||
|
from aiogram.types import CallbackQuery
|
||||||
|
from logs.custom_logger import logger
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
def handle_callback_errors(func: Callable[..., Any]) -> Callable[..., Any]:
|
||||||
|
"""Декоратор для обработки ошибок в callback handlers."""
|
||||||
|
@wraps(func)
|
||||||
|
async def wrapper(call: CallbackQuery, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
return await func(call, *args, **kwargs)
|
||||||
|
except UserBlockedBotError:
|
||||||
|
await call.answer(
|
||||||
|
text=MESSAGE_ERROR,
|
||||||
|
show_alert=True,
|
||||||
|
cache_time=3
|
||||||
|
)
|
||||||
|
except (PostNotFoundError, PublishError) as e:
|
||||||
|
logger.error(f'Ошибка в {func.__name__}: {str(e)}')
|
||||||
|
await call.answer(
|
||||||
|
text=MESSAGE_ERROR,
|
||||||
|
show_alert=True,
|
||||||
|
cache_time=3
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
if str(e) == ERROR_BOT_BLOCKED:
|
||||||
|
await call.answer(
|
||||||
|
text=MESSAGE_ERROR,
|
||||||
|
show_alert=True,
|
||||||
|
cache_time=3
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Получить settings из kwargs или через dependency injection
|
||||||
|
settings = kwargs.get('settings')
|
||||||
|
if settings:
|
||||||
|
important_logs = settings['Telegram']['important_logs']
|
||||||
|
await call.bot.send_message(
|
||||||
|
chat_id=important_logs,
|
||||||
|
text=f"Произошла ошибка в {func.__name__}: {str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
||||||
|
)
|
||||||
|
logger.error(f'Неожиданная ошибка в {func.__name__}: {str(e)}')
|
||||||
|
await call.answer(
|
||||||
|
text=MESSAGE_ERROR,
|
||||||
|
show_alert=True,
|
||||||
|
cache_time=3
|
||||||
|
)
|
||||||
|
return wrapper
|
||||||
|
```
|
||||||
|
|
||||||
|
**Использование:**
|
||||||
|
```python
|
||||||
|
@callback_router.callback_query(F.data == CALLBACK_APPROVE)
|
||||||
|
@handle_callback_errors
|
||||||
|
@track_time("post_for_group", "callback_handlers")
|
||||||
|
@track_errors("callback_handlers", "post_for_group")
|
||||||
|
async def post_for_group(call: CallbackQuery, ...):
|
||||||
|
# Только бизнес-логика, без try-except
|
||||||
|
publish_service = get_post_publish_service()
|
||||||
|
await publish_service.publish_post(call)
|
||||||
|
await call.answer(text=MESSAGE_PUBLISHED, cache_time=3)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Файлы для изменения:**
|
||||||
|
- Создать `helper_bot/handlers/callback/decorators.py`
|
||||||
|
- Рефакторинг `helper_bot/handlers/callback/callback_handlers.py`
|
||||||
|
|
||||||
|
**Оценка:** Средняя сложность, требует тестирования всех сценариев
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. Валидация настроек при старте
|
||||||
|
|
||||||
|
**Статус:** ⬜
|
||||||
|
|
||||||
|
**Проблема:**
|
||||||
|
Настройки загружаются из `.env` без валидации. Отсутствие обязательных настроек обнаруживается только во время выполнения, что затрудняет отладку.
|
||||||
|
|
||||||
|
**Текущее состояние:**
|
||||||
|
```python
|
||||||
|
# helper_bot/utils/base_dependency_factory.py
|
||||||
|
def _load_settings_from_env(self):
|
||||||
|
self.settings['Telegram'] = {
|
||||||
|
'bot_token': os.getenv('BOT_TOKEN', ''), # Может быть пустой строкой
|
||||||
|
# ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Рекомендация:**
|
||||||
|
Добавить валидацию обязательных настроек:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class BaseDependencyFactory:
|
||||||
|
REQUIRED_SETTINGS = {
|
||||||
|
'Telegram': ['bot_token'],
|
||||||
|
'S3': ['endpoint_url', 'access_key', 'secret_key', 'bucket_name'] # Если S3 включен
|
||||||
|
}
|
||||||
|
|
||||||
|
def _validate_settings(self):
|
||||||
|
"""Валидирует обязательные настройки."""
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
# Проверка Telegram настроек
|
||||||
|
for key in self.REQUIRED_SETTINGS['Telegram']:
|
||||||
|
value = self.settings['Telegram'].get(key)
|
||||||
|
if not value:
|
||||||
|
errors.append(f"Telegram.{key} is required but not set")
|
||||||
|
|
||||||
|
# Проверка S3 настроек (если включен)
|
||||||
|
if self.settings['S3']['enabled']:
|
||||||
|
for key in self.REQUIRED_SETTINGS['S3']:
|
||||||
|
value = self.settings['S3'].get(key)
|
||||||
|
if not value:
|
||||||
|
errors.append(f"S3.{key} is required when S3 is enabled but not set")
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
error_msg = "Configuration errors:\n" + "\n".join(f" - {e}" for e in errors)
|
||||||
|
raise ValueError(error_msg)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# ... существующий код ...
|
||||||
|
self._load_settings_from_env()
|
||||||
|
self._validate_settings() # Добавить валидацию
|
||||||
|
self._init_s3_storage()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Файлы для изменения:**
|
||||||
|
- `helper_bot/utils/base_dependency_factory.py`
|
||||||
|
|
||||||
|
**Оценка:** Низкая сложность, быстрое добавление
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. Исправление RepositoryFactory
|
||||||
|
|
||||||
|
**Статус:** ⬜
|
||||||
|
|
||||||
|
**Проблема:**
|
||||||
|
Методы `check_database_integrity()` и `cleanup_wal_files()` в `RepositoryFactory` вызываются только для репозитория `users`, хотя должны применяться ко всем репозиториям или к базе данных в целом.
|
||||||
|
|
||||||
|
**Текущее состояние:**
|
||||||
|
```python
|
||||||
|
# database/repository_factory.py
|
||||||
|
async def check_database_integrity(self):
|
||||||
|
"""Проверяет целостность базы данных."""
|
||||||
|
await self.users.check_database_integrity() # Только users?
|
||||||
|
|
||||||
|
async def cleanup_wal_files(self):
|
||||||
|
"""Очищает WAL файлы."""
|
||||||
|
await self.users.cleanup_wal_files() # Только users?
|
||||||
|
```
|
||||||
|
|
||||||
|
**Рекомендация:**
|
||||||
|
Проверка целостности и очистка WAL должны выполняться один раз для всей БД, а не для каждого репозитория:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def check_database_integrity(self):
|
||||||
|
"""Проверяет целостность базы данных."""
|
||||||
|
# Использовать любой репозиторий для доступа к БД
|
||||||
|
await self.users.check_database_integrity()
|
||||||
|
|
||||||
|
async def cleanup_wal_files(self):
|
||||||
|
"""Очищает WAL файлы."""
|
||||||
|
# Использовать любой репозиторий для доступа к БД
|
||||||
|
await self.users.cleanup_wal_files()
|
||||||
|
```
|
||||||
|
|
||||||
|
Или лучше - вынести эти методы в `DatabaseConnection` и вызывать через любой репозиторий (текущая реализация уже правильная, но можно улучшить документацию).
|
||||||
|
|
||||||
|
**Альтернатива:** Создать отдельный класс `DatabaseManager` для операций на уровне БД.
|
||||||
|
|
||||||
|
**Файлы для изменения:**
|
||||||
|
- `database/repository_factory.py` (улучшить документацию)
|
||||||
|
- Возможно создать `database/database_manager.py`
|
||||||
|
|
||||||
|
**Оценка:** Низкая сложность, в основном документация
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟢 Низкий приоритет
|
||||||
|
|
||||||
|
### 8. Добавление кэширования (Redis)
|
||||||
|
|
||||||
|
**Статус:** ⬜
|
||||||
|
|
||||||
|
**Проблема:**
|
||||||
|
Часто запрашиваемые данные (например, список администраторов, настройки пользователей) загружаются из БД при каждом запросе, что создает лишнюю нагрузку на базу данных.
|
||||||
|
|
||||||
|
**Рекомендация:**
|
||||||
|
Добавить Redis для кэширования часто используемых данных:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# helper_bot/utils/cache.py
|
||||||
|
import redis.asyncio as redis
|
||||||
|
from typing import Optional, Any
|
||||||
|
import json
|
||||||
|
from helper_bot.utils.base_dependency_factory import get_global_instance
|
||||||
|
|
||||||
|
class CacheService:
|
||||||
|
def __init__(self):
|
||||||
|
bdf = get_global_instance()
|
||||||
|
settings = bdf.get_settings()
|
||||||
|
self.redis_client = None
|
||||||
|
|
||||||
|
if settings.get('Redis', {}).get('enabled', False):
|
||||||
|
self.redis_client = redis.from_url(
|
||||||
|
settings['Redis']['url'],
|
||||||
|
decode_responses=True
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get(self, key: str) -> Optional[Any]:
|
||||||
|
"""Получить значение из кэша."""
|
||||||
|
if not self.redis_client:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
value = await self.redis_client.get(key)
|
||||||
|
if value:
|
||||||
|
return json.loads(value)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка получения из кэша: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def set(self, key: str, value: Any, ttl: int = 3600):
|
||||||
|
"""Установить значение в кэш."""
|
||||||
|
if not self.redis_client:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self.redis_client.setex(
|
||||||
|
key,
|
||||||
|
ttl,
|
||||||
|
json.dumps(value)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка записи в кэш: {e}")
|
||||||
|
|
||||||
|
async def delete(self, key: str):
|
||||||
|
"""Удалить значение из кэша."""
|
||||||
|
if not self.redis_client:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self.redis_client.delete(key)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка удаления из кэша: {e}")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Использование:**
|
||||||
|
```python
|
||||||
|
# В репозиториях или сервисах
|
||||||
|
cache = CacheService()
|
||||||
|
|
||||||
|
# Получение с кэшированием
|
||||||
|
async def get_admin_list(self):
|
||||||
|
cache_key = "admin_list"
|
||||||
|
cached = await cache.get(cache_key)
|
||||||
|
if cached:
|
||||||
|
return cached
|
||||||
|
|
||||||
|
# Загрузка из БД
|
||||||
|
admins = await self._load_from_db()
|
||||||
|
|
||||||
|
# Сохранение в кэш на 1 час
|
||||||
|
await cache.set(cache_key, admins, ttl=3600)
|
||||||
|
return admins
|
||||||
|
```
|
||||||
|
|
||||||
|
**Данные для кэширования:**
|
||||||
|
- Список администраторов
|
||||||
|
- Настройки пользователей (если редко меняются)
|
||||||
|
- Статистика (активные пользователи за день)
|
||||||
|
- Черный список (с коротким TTL)
|
||||||
|
|
||||||
|
**Файлы для изменения:**
|
||||||
|
- Создать `helper_bot/utils/cache.py`
|
||||||
|
- Добавить настройки Redis в `BaseDependencyFactory`
|
||||||
|
- Обновить репозитории для использования кэша
|
||||||
|
|
||||||
|
**Оценка:** Средняя сложность, требует настройки Redis инфраструктуры
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9. Улучшение Type Hints
|
||||||
|
|
||||||
|
**Статус:** ⬜
|
||||||
|
|
||||||
|
**Проблема:**
|
||||||
|
Некоторые методы возвращают `dict` без указания структуры, что затрудняет понимание API и использование IDE.
|
||||||
|
|
||||||
|
**Пример:**
|
||||||
|
```python
|
||||||
|
def get_settings(self):
|
||||||
|
return self.settings # Какой тип? Dict[str, Any]?
|
||||||
|
```
|
||||||
|
|
||||||
|
**Рекомендация:**
|
||||||
|
Использовать `TypedDict` для структурированных словарей:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from typing import TypedDict, Dict, Any
|
||||||
|
|
||||||
|
class TelegramSettings(TypedDict):
|
||||||
|
bot_token: str
|
||||||
|
listen_bot_token: str
|
||||||
|
preview_link: bool
|
||||||
|
main_public: str
|
||||||
|
group_for_posts: int
|
||||||
|
# ...
|
||||||
|
|
||||||
|
class SettingsDict(TypedDict):
|
||||||
|
Telegram: TelegramSettings
|
||||||
|
Settings: Dict[str, bool]
|
||||||
|
Metrics: Dict[str, Any]
|
||||||
|
S3: Dict[str, Any]
|
||||||
|
|
||||||
|
class BaseDependencyFactory:
|
||||||
|
def get_settings(self) -> SettingsDict:
|
||||||
|
return self.settings
|
||||||
|
```
|
||||||
|
|
||||||
|
**Файлы для изменения:**
|
||||||
|
- `helper_bot/utils/base_dependency_factory.py`
|
||||||
|
- Создать `helper_bot/utils/types.py` для типов
|
||||||
|
|
||||||
|
**Оценка:** Средняя сложность, требует обновления всех мест использования
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10. Расширение тестового покрытия
|
||||||
|
|
||||||
|
**Статус:** ⬜
|
||||||
|
|
||||||
|
**Проблема:**
|
||||||
|
Некоторые компоненты не покрыты тестами или имеют недостаточное покрытие.
|
||||||
|
|
||||||
|
**Рекомендация:**
|
||||||
|
Добавить тесты для:
|
||||||
|
|
||||||
|
1. **Middleware:**
|
||||||
|
- `DependenciesMiddleware` - проверка внедрения зависимостей
|
||||||
|
- `BlacklistMiddleware` - проверка блокировки пользователей
|
||||||
|
- `RateLimitMiddleware` - проверка ограничений
|
||||||
|
|
||||||
|
2. **BaseDependencyFactory:**
|
||||||
|
- Инициализация с валидными настройками
|
||||||
|
- Инициализация с невалидными настройками
|
||||||
|
- Получение зависимостей
|
||||||
|
|
||||||
|
3. **Интеграционные тесты:**
|
||||||
|
- Полные сценарии обработки сообщений
|
||||||
|
- Сценарии с ошибками
|
||||||
|
- Сценарии с rate limiting
|
||||||
|
|
||||||
|
**Файлы для создания:**
|
||||||
|
- `tests/test_dependencies_middleware.py`
|
||||||
|
- `tests/test_base_dependency_factory.py`
|
||||||
|
- `tests/test_integration_handlers.py`
|
||||||
|
|
||||||
|
**Оценка:** Высокая сложность, требует времени на написание тестов
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11. Улучшение логирования
|
||||||
|
|
||||||
|
**Статус:** ⬜
|
||||||
|
|
||||||
|
**Проблема:**
|
||||||
|
В коде много `logger.info()` там, где можно использовать `logger.debug()` для детальной отладки. Это приводит к засорению логов в production.
|
||||||
|
|
||||||
|
**Рекомендация:**
|
||||||
|
Пересмотреть уровни логирования:
|
||||||
|
|
||||||
|
- `logger.debug()` - детальная отладочная информация (шаги выполнения, промежуточные значения)
|
||||||
|
- `logger.info()` - важные события (старт/остановка бота, критические действия пользователей)
|
||||||
|
- `logger.warning()` - предупреждения (нестандартные ситуации, которые не критичны)
|
||||||
|
- `logger.error()` - ошибки (исключения, сбои)
|
||||||
|
|
||||||
|
**Примеры для изменения:**
|
||||||
|
```python
|
||||||
|
# Было
|
||||||
|
logger.info(f"DependenciesMiddleware: внедрены зависимости для {type(event).__name__}")
|
||||||
|
|
||||||
|
# Стало
|
||||||
|
logger.debug(f"DependenciesMiddleware: внедрены зависимости для {type(event).__name__}")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Файлы для изменения:**
|
||||||
|
- Все файлы с избыточным `logger.info()`
|
||||||
|
|
||||||
|
**Оценка:** Низкая сложность, но требует времени на ревью всех логов
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 12. Документация проекта
|
||||||
|
|
||||||
|
**Статус:** ⬜
|
||||||
|
|
||||||
|
**Проблема:**
|
||||||
|
Отсутствует общая документация проекта, что затрудняет onboarding новых разработчиков.
|
||||||
|
|
||||||
|
**Рекомендация:**
|
||||||
|
Создать следующие документы:
|
||||||
|
|
||||||
|
1. **README.md** (в корне проекта):
|
||||||
|
- Описание проекта
|
||||||
|
- Требования
|
||||||
|
- Установка и настройка
|
||||||
|
- Запуск
|
||||||
|
- Структура проекта
|
||||||
|
|
||||||
|
2. **docs/ARCHITECTURE.md**:
|
||||||
|
- Детальное описание архитектуры
|
||||||
|
- Диаграммы компонентов
|
||||||
|
- Паттерны проектирования
|
||||||
|
|
||||||
|
3. **docs/DEPLOYMENT.md**:
|
||||||
|
- Инструкции по развертыванию
|
||||||
|
- Настройка окружения
|
||||||
|
- Мониторинг
|
||||||
|
|
||||||
|
4. **docs/DEVELOPMENT.md**:
|
||||||
|
- Руководство для разработчиков
|
||||||
|
- Процесс разработки
|
||||||
|
- Code style guide (ссылка на .cursor/rules)
|
||||||
|
|
||||||
|
**Оценка:** Средняя сложность, требует времени на написание
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Статистика
|
||||||
|
|
||||||
|
- **Всего задач:** 12
|
||||||
|
- **Высокий приоритет:** 3
|
||||||
|
- **Средний приоритет:** 4
|
||||||
|
- **Низкий приоритет:** 5
|
||||||
|
|
||||||
|
## 📝 Заметки
|
||||||
|
|
||||||
|
- Большинство задач высокого приоритета связаны между собой (стандартизация DI решит несколько TODO)
|
||||||
|
- Задачи среднего приоритета улучшают производительность и качество кода
|
||||||
|
- Задачи низкого приоритета улучшают developer experience и поддерживаемость
|
||||||
|
|
||||||
|
## 🔄 Обновления
|
||||||
|
|
||||||
|
- **2026-01-25:** Создан первоначальный список улучшений на основе анализа кодовой базы
|
||||||
|
- **2026-01-25:** Добавлена задача #8 по кэшированию (Redis)
|
||||||
|
- **2026-01-25:** Создан документ `PYTHON_VERSION_MANAGEMENT.md` с рекомендациями по унификации версий Python
|
||||||
309
docs/OPERATIONS.md
Normal file
309
docs/OPERATIONS.md
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
# Операционные команды для управления ботом
|
||||||
|
|
||||||
|
> **⚠️ ВАЖНО:** Все команды выполняются из корневой директории проекта
|
||||||
|
|
||||||
|
## 🔧 Основные команды
|
||||||
|
|
||||||
|
### Запуск и остановка
|
||||||
|
```bash
|
||||||
|
# Запустить всю инфраструктуру (Prometheus + Бот)
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Запустить только бота
|
||||||
|
docker-compose up -d telegram-bot
|
||||||
|
|
||||||
|
# Запустить только Prometheus
|
||||||
|
docker-compose up -d prometheus
|
||||||
|
|
||||||
|
# Остановить все сервисы
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Остановить только бота
|
||||||
|
docker-compose stop telegram-bot
|
||||||
|
|
||||||
|
# Остановить только Prometheus
|
||||||
|
docker-compose stop prometheus
|
||||||
|
```
|
||||||
|
|
||||||
|
### Сборка
|
||||||
|
```bash
|
||||||
|
# Собрать все контейнеры
|
||||||
|
docker-compose build
|
||||||
|
|
||||||
|
# Собрать только бота
|
||||||
|
docker-compose build telegram-bot
|
||||||
|
|
||||||
|
# Собрать только Prometheus
|
||||||
|
docker-compose build prometheus
|
||||||
|
|
||||||
|
# Пересобрать и запустить все
|
||||||
|
docker-compose up -d --build
|
||||||
|
|
||||||
|
# Пересобрать и запустить только бота
|
||||||
|
docker-compose up -d --build telegram-bot
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Мониторинг и логи
|
||||||
|
|
||||||
|
### Просмотр логов
|
||||||
|
```bash
|
||||||
|
# Логи всех сервисов
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# Логи только бота
|
||||||
|
docker-compose logs -f telegram-bot
|
||||||
|
|
||||||
|
# Логи Prometheus
|
||||||
|
docker-compose logs -f prometheus
|
||||||
|
|
||||||
|
# Логи в реальном времени (последние 100 строк)
|
||||||
|
docker-compose logs -f --tail=100
|
||||||
|
```
|
||||||
|
|
||||||
|
### Статус и здоровье
|
||||||
|
```bash
|
||||||
|
# Статус всех контейнеров
|
||||||
|
docker-compose ps
|
||||||
|
|
||||||
|
# Проверить здоровье всех сервисов
|
||||||
|
docker-compose ps | grep -E "(unhealthy|starting)"
|
||||||
|
|
||||||
|
# Проверить здоровье бота
|
||||||
|
curl -f http://localhost:8080/health || echo "❌ Бот недоступен"
|
||||||
|
|
||||||
|
# Проверить здоровье Prometheus
|
||||||
|
curl -f http://localhost:9090/-/healthy || echo "❌ Prometheus недоступен"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 Управление сервисами
|
||||||
|
|
||||||
|
### Перезапуск
|
||||||
|
```bash
|
||||||
|
# Перезапустить все сервисы
|
||||||
|
docker-compose restart
|
||||||
|
|
||||||
|
# Перезапустить только бота
|
||||||
|
docker-compose restart telegram-bot
|
||||||
|
|
||||||
|
# Перезапустить только Prometheus
|
||||||
|
docker-compose restart prometheus
|
||||||
|
```
|
||||||
|
|
||||||
|
### Обновление
|
||||||
|
```bash
|
||||||
|
# Обновить код и перезапустить
|
||||||
|
git pull origin main && docker-compose up -d --build
|
||||||
|
|
||||||
|
# Обновить только бота
|
||||||
|
git pull origin main && docker-compose up -d --build telegram-bot
|
||||||
|
|
||||||
|
# Обновить только Prometheus
|
||||||
|
docker-compose pull prometheus && docker-compose up -d prometheus
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧪 Тестирование
|
||||||
|
|
||||||
|
### Запуск тестов
|
||||||
|
```bash
|
||||||
|
# Запустить все тесты
|
||||||
|
docker-compose run --rm telegram-bot sh -c "pip install --no-cache-dir -r requirements-dev.txt && pytest"
|
||||||
|
|
||||||
|
# Тесты с покрытием
|
||||||
|
docker-compose run --rm telegram-bot sh -c "pip install --no-cache-dir -r requirements-dev.txt && pytest --cov=helper_bot --cov-report=term-missing"
|
||||||
|
|
||||||
|
# Тесты только бота
|
||||||
|
docker-compose run --rm telegram-bot sh -c "pip install --no-cache-dir -r requirements-dev.txt && pytest helper_bot/"
|
||||||
|
|
||||||
|
# Тесты с HTML отчетом
|
||||||
|
docker-compose run --rm telegram-bot sh -c "pip install --no-cache-dir -r requirements-dev.txt && pytest --cov=helper_bot --cov-report=html"
|
||||||
|
|
||||||
|
# Тесты конкретного модуля
|
||||||
|
docker-compose run --rm telegram-bot sh -c "pip install --no-cache-dir -r requirements-dev.txt && pytest helper_bot/handlers/admin/"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ Разработка
|
||||||
|
|
||||||
|
### Отладка
|
||||||
|
```bash
|
||||||
|
# Проверить версию Python в контейнере
|
||||||
|
docker exec bots_telegram_bot python --version
|
||||||
|
|
||||||
|
# Открыть shell в контейнере бота
|
||||||
|
docker exec -it bots_telegram_bot sh
|
||||||
|
|
||||||
|
# Проверить установленные пакеты
|
||||||
|
docker exec bots_telegram_bot pip list
|
||||||
|
|
||||||
|
# Проверить переменные окружения
|
||||||
|
docker exec bots_telegram_bot env | grep TELEGRAM
|
||||||
|
|
||||||
|
# Проверить логи бота в реальном времени
|
||||||
|
docker exec bots_telegram_bot tail -f /app/logs/helper_bot_$(date +%Y-%m-%d).log
|
||||||
|
```
|
||||||
|
|
||||||
|
### База данных
|
||||||
|
```bash
|
||||||
|
# Создать backup базы
|
||||||
|
tar -czf "backup-$(date +%Y%m%d-%H%M%S).tar.gz" database/ logs/ .env
|
||||||
|
|
||||||
|
# Восстановить из backup
|
||||||
|
tar -xzf backup-20241231-120000.tar.gz
|
||||||
|
|
||||||
|
# Подключиться к базе данных
|
||||||
|
docker exec -it bots_telegram_bot sqlite3 /app/database/tg-bot-database.db
|
||||||
|
|
||||||
|
# Проверить размер базы данных
|
||||||
|
docker exec bots_telegram_bot ls -lh /app/database/tg-bot-database.db
|
||||||
|
|
||||||
|
# Очистить логи (⚠️ ОСТОРОЖНО!)
|
||||||
|
docker exec bots_telegram_bot find /app/logs -name "*.log" -mtime +7 -delete
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚨 Аварийные ситуации
|
||||||
|
|
||||||
|
### Диагностика
|
||||||
|
```bash
|
||||||
|
# Проверить использование ресурсов
|
||||||
|
docker stats --no-stream
|
||||||
|
|
||||||
|
# Проверить сетевые соединения
|
||||||
|
docker network ls
|
||||||
|
docker network inspect prod_bots_network
|
||||||
|
|
||||||
|
# Проверить логи ошибок
|
||||||
|
docker-compose logs | grep -i error
|
||||||
|
|
||||||
|
# Проверить использование диска
|
||||||
|
docker system df
|
||||||
|
|
||||||
|
# Проверить свободное место
|
||||||
|
docker exec bots_telegram_bot df -h
|
||||||
|
```
|
||||||
|
|
||||||
|
### Восстановление
|
||||||
|
```bash
|
||||||
|
# Принудительная перезагрузка всех сервисов
|
||||||
|
docker-compose down && docker-compose up -d
|
||||||
|
|
||||||
|
# Очистка всех контейнеров и образов
|
||||||
|
docker-compose down -v --rmi all
|
||||||
|
docker system prune -f
|
||||||
|
|
||||||
|
# Восстановление из последнего backup
|
||||||
|
ls -t backup-*.tar.gz | head -1 | xargs -I {} tar -xzf {}
|
||||||
|
|
||||||
|
# Принудительная перезагрузка только бота
|
||||||
|
docker-compose stop telegram-bot
|
||||||
|
docker-compose rm -f telegram-bot
|
||||||
|
docker-compose up -d --build telegram-bot
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📱 Доступ к сервисам
|
||||||
|
|
||||||
|
### Веб-интерфейсы
|
||||||
|
- **Prometheus**: http://localhost:9090
|
||||||
|
- **Бот Health**: http://localhost:8080/health
|
||||||
|
- **Бот Metrics**: http://localhost:8080/metrics
|
||||||
|
|
||||||
|
### Полезные команды
|
||||||
|
```bash
|
||||||
|
# Открыть Prometheus в браузере
|
||||||
|
open http://localhost:9090
|
||||||
|
|
||||||
|
# Открыть метрики бота в браузере
|
||||||
|
open http://localhost:8080/metrics
|
||||||
|
|
||||||
|
# Открыть health check бота в браузере
|
||||||
|
open http://localhost:8080/health
|
||||||
|
|
||||||
|
# Проверить доступность сервисов
|
||||||
|
curl -s http://localhost:8080/health | jq . || echo "Бот недоступен"
|
||||||
|
curl -s http://localhost:9090/-/healthy || echo "Prometheus недоступен"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 Отладка проблем
|
||||||
|
|
||||||
|
### Частые проблемы
|
||||||
|
```bash
|
||||||
|
# Бот не отвечает
|
||||||
|
docker-compose restart telegram-bot && docker-compose logs -f telegram-bot
|
||||||
|
|
||||||
|
# Prometheus недоступен
|
||||||
|
docker-compose restart prometheus && curl -f http://localhost:9090/-/healthy
|
||||||
|
|
||||||
|
# Проблемы с базой данных
|
||||||
|
docker exec bots_telegram_bot sqlite3 /app/database/tg-bot-database.db ".tables"
|
||||||
|
|
||||||
|
# Проблемы с сетью
|
||||||
|
docker network inspect prod_bots_network
|
||||||
|
|
||||||
|
# Проблемы с правами доступа
|
||||||
|
docker exec bots_telegram_bot ls -la /app/database/
|
||||||
|
docker exec bots_telegram_bot ls -la /app/logs/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Полезные alias'ы для .bashrc/.zshrc
|
||||||
|
```bash
|
||||||
|
# Добавить в ~/.bashrc или ~/.zshrc
|
||||||
|
alias bot='cd /Users/andrejkatyhin/PycharmProjects/prod && docker-compose'
|
||||||
|
alias bot-logs='cd /Users/andrejkatyhin/PycharmProjects/prod && docker-compose logs -f telegram-bot'
|
||||||
|
alias bot-restart='cd /Users/andrejkatyhin/PycharmProjects/prod && docker-compose restart telegram-bot'
|
||||||
|
alias bot-status='cd /Users/andrejkatyhin/PycharmProjects/prod && docker-compose ps'
|
||||||
|
alias bot-shell='cd /Users/andrejkatyhin/PycharmProjects/prod && docker exec -it bots_telegram_bot sh'
|
||||||
|
alias prometheus='cd /Users/andrejkatyhin/PycharmProjects/prod && docker-compose logs -f prometheus'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 Примеры использования
|
||||||
|
|
||||||
|
### Типичный workflow разработки
|
||||||
|
```bash
|
||||||
|
# 1. Внести изменения в код
|
||||||
|
cd /Users/andrejkatyhin/PycharmProjects/prod/bots/telegram-helper-bot
|
||||||
|
# ... редактируем код ...
|
||||||
|
|
||||||
|
# 2. Пересобрать и перезапустить бота
|
||||||
|
cd /Users/andrejkatyhin/PycharmProjects/prod
|
||||||
|
docker-compose up -d --build telegram-bot
|
||||||
|
|
||||||
|
# 3. Проверить логи
|
||||||
|
docker-compose logs -f telegram-bot
|
||||||
|
|
||||||
|
# 4. Запустить тесты
|
||||||
|
docker-compose run --rm telegram-bot sh -c "pip install --no-cache-dir -r requirements-dev.txt && pytest helper_bot/"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Мониторинг в продакшене
|
||||||
|
```bash
|
||||||
|
# Проверить здоровье всех сервисов
|
||||||
|
docker-compose ps | grep -E "(unhealthy|starting)"
|
||||||
|
|
||||||
|
# Посмотреть статус
|
||||||
|
docker-compose ps
|
||||||
|
|
||||||
|
# Проверить логи ошибок
|
||||||
|
docker-compose logs | grep -i error
|
||||||
|
|
||||||
|
# Создать backup
|
||||||
|
tar -czf "backup-$(date +%Y%m%d-%H%M%S).tar.gz" database/ logs/ .env
|
||||||
|
|
||||||
|
# Проверить метрики
|
||||||
|
curl -s http://localhost:8080/metrics | head -20
|
||||||
|
```
|
||||||
|
|
||||||
|
### Отладка проблем
|
||||||
|
```bash
|
||||||
|
# Бот не запускается
|
||||||
|
docker-compose logs telegram-bot
|
||||||
|
|
||||||
|
# Проверить конфигурацию
|
||||||
|
docker exec bots_telegram_bot cat /app/.env
|
||||||
|
|
||||||
|
# Проверить права доступа к файлам
|
||||||
|
docker exec bots_telegram_bot ls -la /app/
|
||||||
|
|
||||||
|
# Проверить сетевые соединения
|
||||||
|
docker exec bots_telegram_bot netstat -tulpn
|
||||||
|
|
||||||
|
# Проверить процессы
|
||||||
|
docker exec bots_telegram_bot ps aux
|
||||||
|
```
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
name = "telegram-helper-bot"
|
name = "telegram-helper-bot"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
description = "Telegram bot with monitoring and metrics"
|
description = "Telegram bot with monitoring and metrics"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.11"
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
testpaths = ["tests"]
|
testpaths = ["tests"]
|
||||||
|
|||||||
@@ -1,150 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Скрипт для тестирования rate limiting решения
|
|
||||||
"""
|
|
||||||
import asyncio
|
|
||||||
import time
|
|
||||||
from unittest.mock import AsyncMock, MagicMock
|
|
||||||
from aiogram.types import Message, User, Chat
|
|
||||||
|
|
||||||
from helper_bot.utils.rate_limiter import send_with_rate_limit
|
|
||||||
from helper_bot.utils.rate_limit_monitor import rate_limit_monitor, get_rate_limit_summary
|
|
||||||
|
|
||||||
|
|
||||||
async def test_rate_limiting():
|
|
||||||
"""Тестирует rate limiting с имитацией отправки сообщений"""
|
|
||||||
|
|
||||||
print("🚀 Начинаем тестирование rate limiting...")
|
|
||||||
|
|
||||||
# Создаем мок объекты
|
|
||||||
mock_bot = MagicMock()
|
|
||||||
mock_user = User(id=123, is_bot=False, first_name="Test")
|
|
||||||
mock_chat = Chat(id=456, type="private")
|
|
||||||
|
|
||||||
# Создаем Message с bot в конструкторе
|
|
||||||
mock_message = Message(
|
|
||||||
message_id=1,
|
|
||||||
date=int(time.time()),
|
|
||||||
chat=mock_chat,
|
|
||||||
from_user=mock_user,
|
|
||||||
content_type="text",
|
|
||||||
bot=mock_bot
|
|
||||||
)
|
|
||||||
|
|
||||||
# Настраиваем мок для send_voice
|
|
||||||
mock_bot.send_voice = AsyncMock(return_value=MagicMock(message_id=1))
|
|
||||||
|
|
||||||
# Функция для отправки голосового сообщения
|
|
||||||
async def send_voice_test():
|
|
||||||
return await mock_bot.send_voice(
|
|
||||||
chat_id=mock_chat.id,
|
|
||||||
voice="test_voice_id"
|
|
||||||
)
|
|
||||||
|
|
||||||
print("📊 Отправляем 5 сообщений подряд...")
|
|
||||||
|
|
||||||
# Отправляем несколько сообщений подряд
|
|
||||||
start_time = time.time()
|
|
||||||
for i in range(5):
|
|
||||||
print(f" Отправка сообщения {i+1}/5...")
|
|
||||||
try:
|
|
||||||
result = await send_with_rate_limit(send_voice_test, mock_chat.id)
|
|
||||||
print(f" ✅ Сообщение {i+1} отправлено успешно")
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ❌ Ошибка при отправке сообщения {i+1}: {e}")
|
|
||||||
|
|
||||||
end_time = time.time()
|
|
||||||
total_time = end_time - start_time
|
|
||||||
|
|
||||||
print(f"\n⏱️ Общее время выполнения: {total_time:.2f} секунд")
|
|
||||||
print(f"📈 Среднее время на сообщение: {total_time/5:.2f} секунд")
|
|
||||||
|
|
||||||
# Показываем статистику
|
|
||||||
print("\n📊 Статистика rate limiting:")
|
|
||||||
summary = get_rate_limit_summary()
|
|
||||||
for key, value in summary.items():
|
|
||||||
if isinstance(value, float):
|
|
||||||
print(f" {key}: {value:.2f}")
|
|
||||||
else:
|
|
||||||
print(f" {key}: {value}")
|
|
||||||
|
|
||||||
# Показываем детальную статистику
|
|
||||||
print("\n🔍 Детальная статистика:")
|
|
||||||
global_stats = rate_limit_monitor.get_global_stats()
|
|
||||||
print(f" Всего запросов: {global_stats.total_requests}")
|
|
||||||
print(f" Успешных: {global_stats.successful_requests}")
|
|
||||||
print(f" Неудачных: {global_stats.failed_requests}")
|
|
||||||
print(f" Процент успеха: {global_stats.success_rate:.1%}")
|
|
||||||
print(f" Среднее время ожидания: {global_stats.average_wait_time:.2f}с")
|
|
||||||
|
|
||||||
# Проверяем что rate limiting работает
|
|
||||||
if total_time > 8: # Должно занять больше 8 секунд (5 сообщений * 1.6с минимум)
|
|
||||||
print("\n✅ Rate limiting работает корректно - сообщения отправляются с задержкой")
|
|
||||||
else:
|
|
||||||
print("\n⚠️ Rate limiting может работать некорректно - сообщения отправлены слишком быстро")
|
|
||||||
|
|
||||||
print("\n🎉 Тестирование завершено!")
|
|
||||||
|
|
||||||
|
|
||||||
async def test_error_handling():
|
|
||||||
"""Тестирует обработку ошибок"""
|
|
||||||
|
|
||||||
print("\n🧪 Тестируем обработку ошибок...")
|
|
||||||
|
|
||||||
# Создаем мок который будет падать с RetryAfter
|
|
||||||
from aiogram.exceptions import TelegramRetryAfter
|
|
||||||
|
|
||||||
mock_bot = MagicMock()
|
|
||||||
mock_chat = Chat(id=789, type="private")
|
|
||||||
|
|
||||||
call_count = 0
|
|
||||||
async def failing_send():
|
|
||||||
nonlocal call_count
|
|
||||||
call_count += 1
|
|
||||||
if call_count <= 2:
|
|
||||||
raise TelegramRetryAfter(
|
|
||||||
method=MagicMock(),
|
|
||||||
message="Flood control exceeded",
|
|
||||||
retry_after=1
|
|
||||||
)
|
|
||||||
return MagicMock(message_id=call_count)
|
|
||||||
|
|
||||||
mock_bot.send_voice = failing_send
|
|
||||||
|
|
||||||
print("📤 Отправляем сообщение с имитацией RetryAfter ошибки...")
|
|
||||||
|
|
||||||
start_time = time.time()
|
|
||||||
try:
|
|
||||||
result = await send_with_rate_limit(failing_send, mock_chat.id)
|
|
||||||
end_time = time.time()
|
|
||||||
print(f"✅ Сообщение отправлено после {call_count} попыток за {end_time - start_time:.2f}с")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Ошибка: {e}")
|
|
||||||
|
|
||||||
print("🎯 Тест обработки ошибок завершен!")
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
"""Основная функция"""
|
|
||||||
print("🔧 Тестирование решения Flood Control")
|
|
||||||
print("=" * 50)
|
|
||||||
|
|
||||||
# Сбрасываем статистику
|
|
||||||
rate_limit_monitor.reset_stats()
|
|
||||||
|
|
||||||
# Запускаем тесты
|
|
||||||
await test_rate_limiting()
|
|
||||||
await test_error_handling()
|
|
||||||
|
|
||||||
print("\n" + "=" * 50)
|
|
||||||
print("📋 Итоговая статистика:")
|
|
||||||
summary = get_rate_limit_summary()
|
|
||||||
for key, value in summary.items():
|
|
||||||
if isinstance(value, float):
|
|
||||||
print(f" {key}: {value:.2f}")
|
|
||||||
else:
|
|
||||||
print(f" {key}: {value}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(main())
|
|
||||||
@@ -239,7 +239,10 @@ class TestBlacklistRepository:
|
|||||||
blacklist_repository._execute_query_with_result.assert_called_once()
|
blacklist_repository._execute_query_with_result.assert_called_once()
|
||||||
call_args = blacklist_repository._execute_query_with_result.call_args
|
call_args = blacklist_repository._execute_query_with_result.call_args
|
||||||
|
|
||||||
assert call_args[0][0] == "SELECT user_id, message_for_user, date_to_unban, created_at FROM blacklist LIMIT ?, ?"
|
# Нормализуем SQL запрос (убираем лишние пробелы и переносы строк)
|
||||||
|
actual_query = ' '.join(call_args[0][0].split())
|
||||||
|
expected_query = "SELECT user_id, message_for_user, date_to_unban, created_at, ban_author FROM blacklist LIMIT ?, ?"
|
||||||
|
assert actual_query == expected_query
|
||||||
assert call_args[0][1] == (0, 10)
|
assert call_args[0][1] == (0, 10)
|
||||||
|
|
||||||
# Проверяем логирование
|
# Проверяем логирование
|
||||||
@@ -250,10 +253,10 @@ class TestBlacklistRepository:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_all_users_no_limit(self, blacklist_repository):
|
async def test_get_all_users_no_limit(self, blacklist_repository):
|
||||||
"""Тест получения всех пользователей без лимитов"""
|
"""Тест получения всех пользователей без лимитов"""
|
||||||
# Симулируем результат запроса
|
# Симулируем результат запроса (теперь включает ban_author)
|
||||||
mock_rows = [
|
mock_rows = [
|
||||||
(12345, "Нарушение правил", int(time.time()) + 86400, int(time.time())),
|
(12345, "Нарушение правил", int(time.time()) + 86400, int(time.time()), 999),
|
||||||
(67890, "Постоянный бан", None, int(time.time()) - 86400)
|
(67890, "Постоянный бан", None, int(time.time()) - 86400, None)
|
||||||
]
|
]
|
||||||
blacklist_repository._execute_query_with_result.return_value = mock_rows
|
blacklist_repository._execute_query_with_result.return_value = mock_rows
|
||||||
|
|
||||||
@@ -266,7 +269,10 @@ class TestBlacklistRepository:
|
|||||||
blacklist_repository._execute_query_with_result.assert_called_once()
|
blacklist_repository._execute_query_with_result.assert_called_once()
|
||||||
call_args = blacklist_repository._execute_query_with_result.call_args
|
call_args = blacklist_repository._execute_query_with_result.call_args
|
||||||
|
|
||||||
assert call_args[0][0] == "SELECT user_id, message_for_user, date_to_unban, created_at FROM blacklist"
|
# Нормализуем SQL запрос (убираем лишние пробелы и переносы строк)
|
||||||
|
actual_query = ' '.join(call_args[0][0].split())
|
||||||
|
expected_query = "SELECT user_id, message_for_user, date_to_unban, created_at, ban_author FROM blacklist"
|
||||||
|
assert actual_query == expected_query
|
||||||
# Проверяем, что параметры пустые (без лимитов)
|
# Проверяем, что параметры пустые (без лимитов)
|
||||||
assert len(call_args[0]) == 1 # Только SQL запрос, без параметров
|
assert len(call_args[0]) == 1 # Только SQL запрос, без параметров
|
||||||
|
|
||||||
|
|||||||
@@ -258,16 +258,15 @@ class TestSendMediaGroupMessageToPrivateChat:
|
|||||||
|
|
||||||
# Мокаем БД
|
# Мокаем БД
|
||||||
mock_db = AsyncMock()
|
mock_db = AsyncMock()
|
||||||
mock_db.add_post = AsyncMock()
|
|
||||||
|
|
||||||
with patch('helper_bot.utils.helper_func.add_in_db_media_mediagroup', return_value=True):
|
with patch('helper_bot.utils.helper_func.add_in_db_media_mediagroup', return_value=True):
|
||||||
result = await send_media_group_message_to_private_chat(
|
with patch('asyncio.create_task'): # Мокаем create_task, чтобы фоновая задача не выполнялась
|
||||||
100, mock_message, [], mock_db, main_post_id=789
|
result = await send_media_group_message_to_private_chat(
|
||||||
)
|
100, mock_message, [], mock_db, main_post_id=789
|
||||||
|
)
|
||||||
assert result == 456
|
|
||||||
mock_message.bot.send_media_group.assert_called_once()
|
assert result == [456] # Функция возвращает список message_id
|
||||||
mock_db.add_post.assert_called_once()
|
mock_message.bot.send_media_group.assert_called_once()
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_send_media_group_message_media_processing_fails(self):
|
async def test_send_media_group_message_media_processing_fails(self):
|
||||||
@@ -285,16 +284,15 @@ class TestSendMediaGroupMessageToPrivateChat:
|
|||||||
|
|
||||||
# Мокаем БД
|
# Мокаем БД
|
||||||
mock_db = AsyncMock()
|
mock_db = AsyncMock()
|
||||||
mock_db.add_post = AsyncMock()
|
|
||||||
|
|
||||||
with patch('helper_bot.utils.helper_func.add_in_db_media_mediagroup', return_value=False):
|
with patch('helper_bot.utils.helper_func.add_in_db_media_mediagroup', return_value=False):
|
||||||
result = await send_media_group_message_to_private_chat(
|
with patch('asyncio.create_task'): # Мокаем create_task, чтобы фоновая задача не выполнялась
|
||||||
100, mock_message, [], mock_db, main_post_id=789
|
result = await send_media_group_message_to_private_chat(
|
||||||
)
|
100, mock_message, [], mock_db, main_post_id=789
|
||||||
|
)
|
||||||
assert result == 456 # Функция все равно возвращает message_id
|
|
||||||
mock_message.bot.send_media_group.assert_called_once()
|
assert result == [456] # Функция возвращает список message_id
|
||||||
mock_db.add_post.assert_called_once()
|
mock_message.bot.send_media_group.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -62,33 +62,38 @@ class TestPostRepository:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_create_tables(self, post_repository):
|
async def test_create_tables(self, post_repository):
|
||||||
"""Тест создания таблиц."""
|
"""Тест создания таблиц."""
|
||||||
# Мокаем _execute_query
|
# Мокаем _execute_query и _execute_query_with_result
|
||||||
post_repository._execute_query = AsyncMock()
|
post_repository._execute_query = AsyncMock()
|
||||||
|
post_repository._execute_query_with_result = AsyncMock(return_value=[]) # Для проверки столбца
|
||||||
|
|
||||||
await post_repository.create_tables()
|
await post_repository.create_tables()
|
||||||
|
|
||||||
# Проверяем, что create_tables вызвался 3 раза (для каждой таблицы)
|
# Проверяем, что create_tables вызвался минимум 3 раза (для каждой таблицы)
|
||||||
assert post_repository._execute_query.call_count == 3
|
# Может быть больше из-за ALTER TABLE и индексов
|
||||||
|
assert post_repository._execute_query.call_count >= 3
|
||||||
|
|
||||||
|
# Проверяем, что все нужные таблицы созданы (порядок может быть разным из-за ALTER TABLE)
|
||||||
|
calls = post_repository._execute_query.call_args_list
|
||||||
|
all_queries = [call[0][0] for call in calls]
|
||||||
|
|
||||||
# Проверяем создание таблицы постов
|
# Проверяем создание таблицы постов
|
||||||
calls = post_repository._execute_query.call_args_list
|
post_table_queries = [q for q in all_queries if "CREATE TABLE IF NOT EXISTS post_from_telegram_suggest" in q]
|
||||||
post_table_call = calls[0][0][0]
|
assert len(post_table_queries) > 0
|
||||||
assert "CREATE TABLE IF NOT EXISTS post_from_telegram_suggest" in post_table_call
|
assert "message_id INTEGER NOT NULL PRIMARY KEY" in post_table_queries[0]
|
||||||
assert "message_id INTEGER NOT NULL PRIMARY KEY" in post_table_call
|
assert "created_at INTEGER NOT NULL" in post_table_queries[0]
|
||||||
assert "created_at INTEGER NOT NULL" in post_table_call
|
assert "status TEXT NOT NULL DEFAULT 'suggest'" in post_table_queries[0]
|
||||||
assert "status TEXT NOT NULL DEFAULT 'suggest'" in post_table_call
|
assert "is_anonymous INTEGER" in post_table_queries[0]
|
||||||
assert "is_anonymous INTEGER" in post_table_call
|
assert "FOREIGN KEY (author_id) REFERENCES our_users (user_id) ON DELETE CASCADE" in post_table_queries[0]
|
||||||
assert "FOREIGN KEY (author_id) REFERENCES our_users (user_id) ON DELETE CASCADE" in post_table_call
|
|
||||||
|
|
||||||
# Проверяем создание таблицы контента
|
# Проверяем создание таблицы контента
|
||||||
content_table_call = calls[1][0][0]
|
content_table_queries = [q for q in all_queries if "CREATE TABLE IF NOT EXISTS content_post_from_telegram" in q]
|
||||||
assert "CREATE TABLE IF NOT EXISTS content_post_from_telegram" in content_table_call
|
assert len(content_table_queries) > 0
|
||||||
assert "PRIMARY KEY (message_id, content_name)" in content_table_call
|
assert "PRIMARY KEY (message_id, content_name)" in content_table_queries[0]
|
||||||
|
|
||||||
# Проверяем создание таблицы связей
|
# Проверяем создание таблицы связей
|
||||||
link_table_call = calls[2][0][0]
|
link_table_queries = [q for q in all_queries if "CREATE TABLE IF NOT EXISTS message_link_to_content" in q]
|
||||||
assert "CREATE TABLE IF NOT EXISTS message_link_to_content" in link_table_call
|
assert len(link_table_queries) > 0
|
||||||
assert "PRIMARY KEY (post_id, message_id)" in link_table_call
|
assert "PRIMARY KEY (post_id, message_id)" in link_table_queries[0]
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_add_post_with_date(self, post_repository, sample_post):
|
async def test_add_post_with_date(self, post_repository, sample_post):
|
||||||
@@ -103,7 +108,7 @@ class TestPostRepository:
|
|||||||
query = call_args[0][0]
|
query = call_args[0][0]
|
||||||
params = call_args[0][1]
|
params = call_args[0][1]
|
||||||
|
|
||||||
assert "INSERT INTO post_from_telegram_suggest" in query
|
assert "INSERT OR IGNORE INTO post_from_telegram_suggest" in query
|
||||||
assert "status" in query
|
assert "status" in query
|
||||||
assert "is_anonymous" in query
|
assert "is_anonymous" in query
|
||||||
assert "VALUES (?, ?, ?, ?, ?, ?)" in query
|
assert "VALUES (?, ?, ?, ?, ?, ?)" in query
|
||||||
@@ -148,9 +153,11 @@ class TestPostRepository:
|
|||||||
|
|
||||||
await post_repository.add_post(sample_post)
|
await post_repository.add_post(sample_post)
|
||||||
|
|
||||||
post_repository.logger.info.assert_called_once_with(
|
# Проверяем, что логирование вызвано с новым форматом сообщения
|
||||||
f"Пост добавлен: message_id={sample_post.message_id}"
|
post_repository.logger.info.assert_called_once()
|
||||||
)
|
log_call = post_repository.logger.info.call_args[0][0]
|
||||||
|
assert f"message_id={sample_post.message_id}" in log_call
|
||||||
|
assert "Пост добавлен" in log_call or "уже существует" in log_call
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_update_helper_message(self, post_repository):
|
async def test_update_helper_message(self, post_repository):
|
||||||
@@ -174,29 +181,61 @@ class TestPostRepository:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_update_status_by_message_id(self, post_repository):
|
async def test_update_status_by_message_id(self, post_repository):
|
||||||
"""Тест обновления статуса поста по message_id."""
|
"""Тест обновления статуса поста по message_id."""
|
||||||
|
# Создаем таблицы перед тестом
|
||||||
post_repository._execute_query = AsyncMock()
|
post_repository._execute_query = AsyncMock()
|
||||||
|
post_repository._execute_query_with_result = AsyncMock(return_value=[])
|
||||||
|
post_repository._get_connection = AsyncMock()
|
||||||
|
mock_conn = AsyncMock()
|
||||||
|
mock_cur = AsyncMock()
|
||||||
|
mock_cur.fetchone = AsyncMock(return_value=(1,)) # 1 строка обновлена
|
||||||
|
mock_conn.execute = AsyncMock(return_value=mock_cur)
|
||||||
|
post_repository._get_connection.return_value = mock_conn
|
||||||
post_repository.logger = MagicMock()
|
post_repository.logger = MagicMock()
|
||||||
|
|
||||||
|
# Создаем таблицы
|
||||||
|
await post_repository.create_tables()
|
||||||
|
post_repository._execute_query.reset_mock()
|
||||||
|
post_repository._execute_query_with_result.reset_mock()
|
||||||
|
post_repository.logger.info.reset_mock() # Сбрасываем счетчик логирования
|
||||||
|
|
||||||
message_id = 12345
|
message_id = 12345
|
||||||
status = "approved"
|
status = "approved"
|
||||||
|
|
||||||
await post_repository.update_status_by_message_id(message_id, status)
|
await post_repository.update_status_by_message_id(message_id, status)
|
||||||
|
|
||||||
post_repository._execute_query.assert_called_once()
|
# Проверяем, что conn.execute был вызван с правильными параметрами
|
||||||
call_args = post_repository._execute_query.call_args
|
assert mock_conn.execute.call_count >= 1
|
||||||
query = call_args[0][0]
|
update_call = mock_conn.execute.call_args_list[0]
|
||||||
params = call_args[0][1]
|
query = update_call[0][0]
|
||||||
|
params = update_call[0][1]
|
||||||
|
|
||||||
assert "UPDATE post_from_telegram_suggest" in query
|
assert "UPDATE post_from_telegram_suggest" in query
|
||||||
assert "SET status = ? WHERE message_id = ?" in query
|
assert "SET status = ? WHERE message_id = ?" in query
|
||||||
assert params == (status, message_id)
|
assert params == (status, message_id)
|
||||||
post_repository.logger.info.assert_called_once()
|
# Проверяем, что после создания таблиц было вызвано логирование обновления статуса
|
||||||
|
post_repository.logger.info.assert_called()
|
||||||
|
log_calls = [str(call) for call in post_repository.logger.info.call_args_list]
|
||||||
|
assert any("Статус поста message_id=12345 обновлён на approved" in str(call) for call in post_repository.logger.info.call_args_list)
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_update_status_for_media_group_by_helper_id(self, post_repository):
|
async def test_update_status_for_media_group_by_helper_id(self, post_repository):
|
||||||
"""Тест обновления статуса медиагруппы по helper_message_id."""
|
"""Тест обновления статуса медиагруппы по helper_message_id."""
|
||||||
|
# Создаем таблицы перед тестом
|
||||||
post_repository._execute_query = AsyncMock()
|
post_repository._execute_query = AsyncMock()
|
||||||
|
post_repository._execute_query_with_result = AsyncMock(return_value=[])
|
||||||
|
post_repository._get_connection = AsyncMock()
|
||||||
|
mock_conn = AsyncMock()
|
||||||
|
mock_cur = AsyncMock()
|
||||||
|
mock_cur.fetchone = AsyncMock(return_value=(1,)) # 1 строка обновлена
|
||||||
|
mock_conn.execute = AsyncMock(return_value=mock_cur)
|
||||||
|
post_repository._get_connection.return_value = mock_conn
|
||||||
post_repository.logger = MagicMock()
|
post_repository.logger = MagicMock()
|
||||||
|
|
||||||
|
# Создаем таблицы
|
||||||
|
await post_repository.create_tables()
|
||||||
|
post_repository._execute_query.reset_mock()
|
||||||
|
post_repository._execute_query_with_result.reset_mock()
|
||||||
|
post_repository.logger.info.reset_mock() # Сбрасываем счетчик логирования
|
||||||
|
|
||||||
helper_message_id = 99999
|
helper_message_id = 99999
|
||||||
status = "declined"
|
status = "declined"
|
||||||
@@ -205,16 +244,19 @@ class TestPostRepository:
|
|||||||
helper_message_id, status
|
helper_message_id, status
|
||||||
)
|
)
|
||||||
|
|
||||||
post_repository._execute_query.assert_called_once()
|
# Проверяем, что conn.execute был вызван с правильными параметрами
|
||||||
call_args = post_repository._execute_query.call_args
|
assert mock_conn.execute.call_count >= 1
|
||||||
query = call_args[0][0]
|
update_call = mock_conn.execute.call_args_list[0]
|
||||||
params = call_args[0][1]
|
query = update_call[0][0]
|
||||||
|
params = update_call[0][1]
|
||||||
|
|
||||||
assert "UPDATE post_from_telegram_suggest" in query
|
assert "UPDATE post_from_telegram_suggest" in query
|
||||||
assert "SET status = ?" in query
|
assert "SET status = ?" in query
|
||||||
assert "message_id = ? OR helper_text_message_id = ?" in query
|
assert "message_id = ? OR helper_text_message_id = ?" in query
|
||||||
assert params == (status, helper_message_id, helper_message_id)
|
assert params == (status, helper_message_id, helper_message_id)
|
||||||
post_repository.logger.info.assert_called_once()
|
# Проверяем, что после создания таблиц было вызвано логирование обновления статуса
|
||||||
|
post_repository.logger.info.assert_called()
|
||||||
|
assert any("Статус медиагруппы helper_message_id=99999 обновлён на declined" in str(call) for call in post_repository.logger.info.call_args_list)
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_add_post_content_success(self, post_repository):
|
async def test_add_post_content_success(self, post_repository):
|
||||||
@@ -648,10 +690,12 @@ class TestPostRepository:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_create_tables_logs_success(self, post_repository):
|
async def test_create_tables_logs_success(self, post_repository):
|
||||||
"""Тест логирования успешного создания таблиц."""
|
"""Тест логирования успешного создания таблиц."""
|
||||||
# Мокаем _execute_query и logger
|
# Мокаем _execute_query, _execute_query_with_result и logger
|
||||||
post_repository._execute_query = AsyncMock()
|
post_repository._execute_query = AsyncMock()
|
||||||
|
post_repository._execute_query_with_result = AsyncMock(return_value=[]) # Для проверки столбца
|
||||||
post_repository.logger = MagicMock()
|
post_repository.logger = MagicMock()
|
||||||
|
|
||||||
await post_repository.create_tables()
|
await post_repository.create_tables()
|
||||||
|
|
||||||
post_repository.logger.info.assert_called_once_with("Таблицы для постов созданы")
|
# Проверяем, что финальное сообщение о создании таблиц было вызвано
|
||||||
|
post_repository.logger.info.assert_any_call("Таблицы для постов созданы")
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class TestPostService:
|
|||||||
db.add_post = AsyncMock()
|
db.add_post = AsyncMock()
|
||||||
db.update_helper_message = AsyncMock()
|
db.update_helper_message = AsyncMock()
|
||||||
db.get_user_by_id = AsyncMock()
|
db.get_user_by_id = AsyncMock()
|
||||||
|
db.add_message_link = AsyncMock()
|
||||||
return db
|
return db
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -60,8 +61,11 @@ class TestPostService:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_handle_text_post_saves_raw_text(self, post_service, mock_message, mock_db):
|
async def test_handle_text_post_saves_raw_text(self, post_service, mock_message, mock_db):
|
||||||
"""Test that handle_text_post saves raw text to database"""
|
"""Test that handle_text_post saves raw text to database"""
|
||||||
|
mock_sent_message = Mock()
|
||||||
|
mock_sent_message.message_id = 200
|
||||||
|
|
||||||
with patch('helper_bot.handlers.private.services.get_text_message', return_value="Formatted text"):
|
with patch('helper_bot.handlers.private.services.get_text_message', return_value="Formatted text"):
|
||||||
with patch('helper_bot.handlers.private.services.send_text_message', return_value=200):
|
with patch('helper_bot.handlers.private.services.send_text_message', return_value=mock_sent_message):
|
||||||
with patch('helper_bot.handlers.private.services.get_first_name', return_value="Test"):
|
with patch('helper_bot.handlers.private.services.get_first_name', return_value="Test"):
|
||||||
with patch('helper_bot.handlers.private.services.get_reply_keyboard_for_post', return_value=None):
|
with patch('helper_bot.handlers.private.services.get_reply_keyboard_for_post', return_value=None):
|
||||||
with patch('helper_bot.handlers.private.services.determine_anonymity', return_value=False):
|
with patch('helper_bot.handlers.private.services.determine_anonymity', return_value=False):
|
||||||
@@ -83,9 +87,11 @@ class TestPostService:
|
|||||||
async def test_handle_text_post_determines_anonymity(self, post_service, mock_message, mock_db):
|
async def test_handle_text_post_determines_anonymity(self, post_service, mock_message, mock_db):
|
||||||
"""Test that handle_text_post determines anonymity correctly"""
|
"""Test that handle_text_post determines anonymity correctly"""
|
||||||
mock_message.text = "Тестовый пост анон"
|
mock_message.text = "Тестовый пост анон"
|
||||||
|
mock_sent_message = Mock()
|
||||||
|
mock_sent_message.message_id = 200
|
||||||
|
|
||||||
with patch('helper_bot.handlers.private.services.get_text_message', return_value="Formatted text"):
|
with patch('helper_bot.handlers.private.services.get_text_message', return_value="Formatted text"):
|
||||||
with patch('helper_bot.handlers.private.services.send_text_message', return_value=200):
|
with patch('helper_bot.handlers.private.services.send_text_message', return_value=mock_sent_message):
|
||||||
with patch('helper_bot.handlers.private.services.get_first_name', return_value="Test"):
|
with patch('helper_bot.handlers.private.services.get_first_name', return_value="Test"):
|
||||||
with patch('helper_bot.handlers.private.services.get_reply_keyboard_for_post', return_value=None):
|
with patch('helper_bot.handlers.private.services.get_reply_keyboard_for_post', return_value=None):
|
||||||
with patch('helper_bot.handlers.private.services.determine_anonymity', return_value=True):
|
with patch('helper_bot.handlers.private.services.determine_anonymity', return_value=True):
|
||||||
@@ -241,14 +247,17 @@ class TestPostService:
|
|||||||
album = [Mock()]
|
album = [Mock()]
|
||||||
album[0].caption = "Медиагруппа подпись"
|
album[0].caption = "Медиагруппа подпись"
|
||||||
|
|
||||||
|
mock_helper_message = Mock()
|
||||||
|
mock_helper_message.message_id = 302
|
||||||
|
|
||||||
with patch('helper_bot.handlers.private.services.get_text_message', return_value="Formatted"):
|
with patch('helper_bot.handlers.private.services.get_text_message', return_value="Formatted"):
|
||||||
with patch('helper_bot.handlers.private.services.prepare_media_group_from_middlewares', return_value=[]):
|
with patch('helper_bot.handlers.private.services.prepare_media_group_from_middlewares', return_value=[]):
|
||||||
with patch('helper_bot.handlers.private.services.send_media_group_message_to_private_chat', return_value=301):
|
with patch('helper_bot.handlers.private.services.send_media_group_message_to_private_chat', return_value=[301]):
|
||||||
with patch('helper_bot.handlers.private.services.get_first_name', return_value="Test"):
|
with patch('helper_bot.handlers.private.services.get_first_name', return_value="Test"):
|
||||||
with patch('helper_bot.handlers.private.services.get_reply_keyboard_for_post', return_value=None):
|
with patch('helper_bot.handlers.private.services.get_reply_keyboard_for_post', return_value=None):
|
||||||
with patch('helper_bot.handlers.private.services.send_text_message', return_value=302):
|
with patch('helper_bot.handlers.private.services.send_text_message', return_value=mock_helper_message):
|
||||||
with patch('helper_bot.handlers.private.services.determine_anonymity', return_value=True):
|
with patch('helper_bot.handlers.private.services.determine_anonymity', return_value=True):
|
||||||
with patch('asyncio.sleep', return_value=None):
|
with patch('asyncio.sleep', new_callable=AsyncMock):
|
||||||
|
|
||||||
await post_service.handle_media_group_post(mock_message, album, "Test")
|
await post_service.handle_media_group_post(mock_message, album, "Test")
|
||||||
|
|
||||||
@@ -257,7 +266,7 @@ class TestPostService:
|
|||||||
main_post = calls[0][0][0]
|
main_post = calls[0][0][0]
|
||||||
|
|
||||||
assert main_post.text == "Медиагруппа подпись" # Raw caption
|
assert main_post.text == "Медиагруппа подпись" # Raw caption
|
||||||
assert main_post.message_id == 300
|
assert main_post.message_id == 301 # Последний message_id из списка
|
||||||
assert main_post.is_anonymous is True
|
assert main_post.is_anonymous is True
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -269,14 +278,17 @@ class TestPostService:
|
|||||||
album = [Mock()]
|
album = [Mock()]
|
||||||
album[0].caption = None
|
album[0].caption = None
|
||||||
|
|
||||||
|
mock_helper_message = Mock()
|
||||||
|
mock_helper_message.message_id = 303
|
||||||
|
|
||||||
with patch('helper_bot.handlers.private.services.get_text_message', return_value=" "):
|
with patch('helper_bot.handlers.private.services.get_text_message', return_value=" "):
|
||||||
with patch('helper_bot.handlers.private.services.prepare_media_group_from_middlewares', return_value=[]):
|
with patch('helper_bot.handlers.private.services.prepare_media_group_from_middlewares', return_value=[]):
|
||||||
with patch('helper_bot.handlers.private.services.send_media_group_message_to_private_chat', return_value=302):
|
with patch('helper_bot.handlers.private.services.send_media_group_message_to_private_chat', return_value=[302]):
|
||||||
with patch('helper_bot.handlers.private.services.get_first_name', return_value="Test"):
|
with patch('helper_bot.handlers.private.services.get_first_name', return_value="Test"):
|
||||||
with patch('helper_bot.handlers.private.services.get_reply_keyboard_for_post', return_value=None):
|
with patch('helper_bot.handlers.private.services.get_reply_keyboard_for_post', return_value=None):
|
||||||
with patch('helper_bot.handlers.private.services.send_text_message', return_value=303):
|
with patch('helper_bot.handlers.private.services.send_text_message', return_value=mock_helper_message):
|
||||||
with patch('helper_bot.handlers.private.services.determine_anonymity', return_value=False):
|
with patch('helper_bot.handlers.private.services.determine_anonymity', return_value=False):
|
||||||
with patch('asyncio.sleep', return_value=None):
|
with patch('asyncio.sleep', new_callable=AsyncMock):
|
||||||
|
|
||||||
await post_service.handle_media_group_post(mock_message, album, "Test")
|
await post_service.handle_media_group_post(mock_message, album, "Test")
|
||||||
|
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ class TestAdminService:
|
|||||||
|
|
||||||
# Act & Assert
|
# Act & Assert
|
||||||
with pytest.raises(UserAlreadyBannedError, match=f"Пользователь {user_id} уже заблокирован"):
|
with pytest.raises(UserAlreadyBannedError, match=f"Пользователь {user_id} уже заблокирован"):
|
||||||
await self.admin_service.ban_user(user_id, username, reason, ban_days)
|
await self.admin_service.ban_user(user_id, username, reason, ban_days, ban_author_id=999)
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_ban_user_permanent(self):
|
async def test_ban_user_permanent(self):
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ class TestHelperFunctions:
|
|||||||
"""Тест функции get_text_message с is_anonymous=True"""
|
"""Тест функции get_text_message с is_anonymous=True"""
|
||||||
text = "Тестовый пост"
|
text = "Тестовый пост"
|
||||||
result = get_text_message(text, "Test", "testuser", is_anonymous=True)
|
result = get_text_message(text, "Test", "testuser", is_anonymous=True)
|
||||||
assert "Пост из ТГ:" in result
|
|
||||||
assert "Тестовый пост" in result
|
assert "Тестовый пост" in result
|
||||||
assert "Пост опубликован анонимно" in result
|
assert "Пост опубликован анонимно" in result
|
||||||
assert "Автор поста" not in result
|
assert "Автор поста" not in result
|
||||||
@@ -98,7 +97,6 @@ class TestHelperFunctions:
|
|||||||
"""Тест функции get_text_message с is_anonymous=False"""
|
"""Тест функции get_text_message с is_anonymous=False"""
|
||||||
text = "Тестовый пост"
|
text = "Тестовый пост"
|
||||||
result = get_text_message(text, "Test", "testuser", is_anonymous=False)
|
result = get_text_message(text, "Test", "testuser", is_anonymous=False)
|
||||||
assert "Пост из ТГ:" in result
|
|
||||||
assert "Тестовый пост" in result
|
assert "Тестовый пост" in result
|
||||||
assert "Автор поста" in result
|
assert "Автор поста" in result
|
||||||
assert "Test" in result
|
assert "Test" in result
|
||||||
@@ -110,14 +108,12 @@ class TestHelperFunctions:
|
|||||||
# Тест с "анон" в тексте
|
# Тест с "анон" в тексте
|
||||||
text = "Тестовый пост анон"
|
text = "Тестовый пост анон"
|
||||||
result = get_text_message(text, "Test", "testuser", is_anonymous=None)
|
result = get_text_message(text, "Test", "testuser", is_anonymous=None)
|
||||||
assert "Пост из ТГ:" in result
|
|
||||||
assert "Тестовый пост анон" in result
|
assert "Тестовый пост анон" in result
|
||||||
assert "Пост опубликован анонимно" in result
|
assert "Пост опубликован анонимно" in result
|
||||||
|
|
||||||
# Тест с "неанон" в тексте
|
# Тест с "неанон" в тексте
|
||||||
text = "Тестовый пост неанон"
|
text = "Тестовый пост неанон"
|
||||||
result = get_text_message(text, "Test", "testuser", is_anonymous=None)
|
result = get_text_message(text, "Test", "testuser", is_anonymous=None)
|
||||||
assert "Пост из ТГ:" in result
|
|
||||||
assert "Тестовый пост неанон" in result
|
assert "Тестовый пост неанон" in result
|
||||||
assert "Автор поста" in result
|
assert "Автор поста" in result
|
||||||
|
|
||||||
@@ -579,13 +575,14 @@ class TestSendMessageFunctions:
|
|||||||
mock_sent_message.message_id = 456
|
mock_sent_message.message_id = 456
|
||||||
mock_message.bot.send_message.return_value = mock_sent_message
|
mock_message.bot.send_message.return_value = mock_sent_message
|
||||||
|
|
||||||
result = await send_text_message(123, mock_message, "Тестовое сообщение")
|
# Мокаем rate_limiter (он импортируется внутри функции)
|
||||||
|
with patch('helper_bot.utils.rate_limiter.send_with_rate_limit', new_callable=AsyncMock) as mock_rate_limit:
|
||||||
assert result == 456
|
mock_rate_limit.return_value = mock_sent_message
|
||||||
mock_message.bot.send_message.assert_called_once_with(
|
|
||||||
chat_id=123,
|
result = await send_text_message(123, mock_message, "Тестовое сообщение")
|
||||||
text="Тестовое сообщение"
|
|
||||||
)
|
assert result == mock_sent_message
|
||||||
|
assert result.message_id == 456
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_send_text_message_with_markup(self):
|
async def test_send_text_message_with_markup(self):
|
||||||
@@ -599,14 +596,14 @@ class TestSendMessageFunctions:
|
|||||||
mock_sent_message.message_id = 456
|
mock_sent_message.message_id = 456
|
||||||
mock_message.bot.send_message.return_value = mock_sent_message
|
mock_message.bot.send_message.return_value = mock_sent_message
|
||||||
|
|
||||||
result = await send_text_message(123, mock_message, "Тестовое сообщение", mock_markup)
|
# Мокаем rate_limiter (он импортируется внутри функции)
|
||||||
|
with patch('helper_bot.utils.rate_limiter.send_with_rate_limit', new_callable=AsyncMock) as mock_rate_limit:
|
||||||
assert result == 456
|
mock_rate_limit.return_value = mock_sent_message
|
||||||
mock_message.bot.send_message.assert_called_once_with(
|
|
||||||
chat_id=123,
|
result = await send_text_message(123, mock_message, "Тестовое сообщение", mock_markup)
|
||||||
text="Тестовое сообщение",
|
|
||||||
reply_markup=mock_markup
|
assert result == mock_sent_message
|
||||||
)
|
assert result.message_id == 456
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_send_photo_message(self):
|
async def test_send_photo_message(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user