WIP: Temporary commit for branch move

This commit is contained in:
2025-08-26 02:14:11 +03:00
parent ee9eafa09f
commit 7b6abe2a0e
44 changed files with 4783 additions and 2383 deletions

34
.gitignore vendored
View File

@@ -2,3 +2,37 @@
/settings.ini /settings.ini
/myenv/ /myenv/
/venv/ /venv/
/.idea/
/logs/*.log
# Testing and coverage files
.coverage
coverage.xml
htmlcov/
.pytest_cache/
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
*.so
# Test database files
database/test.db
test.db
*.db
# IDE and editor files
.vscode/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

73
Makefile Normal file
View File

@@ -0,0 +1,73 @@
.PHONY: help test test-db test-coverage test-html clean install
# Default target
help:
@echo "Available commands:"
@echo " install - Install dependencies"
@echo " test - Run all tests"
@echo " test-db - Run database tests only"
@echo " test-bot - Run bot startup and handler tests only"
@echo " test-media - Run media handler tests only"
@echo " test-errors - Run error handling tests only"
@echo " test-utils - Run utility functions tests only"
@echo " test-keyboards - Run keyboard and filter tests only"
@echo " test-coverage - Run tests with coverage report (helper_bot + database)"
@echo " test-html - Run tests and generate HTML coverage report"
@echo " clean - Clean up generated files"
@echo " coverage - Show coverage report only"
# Install dependencies
install:
python3 -m pip install -r requirements.txt
python3 -m pip install pytest-cov
# Run all tests
test:
python3 -m pytest tests/ -v
# Run database tests only
test-db:
python3 -m pytest tests/test_db.py -v
# Run bot tests only
test-bot:
python3 -m pytest tests/test_bot.py -v
# Run media handler tests only
test-media:
python3 -m pytest tests/test_media_handlers.py -v
# Run error handling tests only
test-errors:
python3 -m pytest tests/test_error_handling.py -v
# Run utils tests only
test-utils:
python3 -m pytest tests/test_utils.py -v
# Run keyboard and filter tests only
test-keyboards:
python3 -m pytest tests/test_keyboards_and_filters.py -v
# Run tests with coverage
test-coverage:
python3 -m pytest tests/ --cov=helper_bot --cov=database --cov-report=term
# Run tests and generate HTML coverage report
test-html:
python3 -m pytest tests/ --cov=helper_bot --cov=database --cov-report=html:htmlcov --cov-report=term
@echo "HTML coverage report generated in htmlcov/index.html"
# Show coverage report only
coverage:
python3 -m coverage report --include="helper_bot/*,database/*"
# Clean up generated files
clean:
rm -rf htmlcov/
rm -f coverage.xml
rm -f .coverage
rm -f database/test.db
rm -f test.db
find . -type d -name "__pycache__" -exec rm -rf {} +
find . -type f -name "*.pyc" -delete

259
README_TESTING.md Normal file
View File

@@ -0,0 +1,259 @@
# Тестирование Telegram Helper Bot
Этот документ описывает систему тестирования для Telegram Helper Bot.
## Структура тестов
Тесты организованы в следующие файлы:
- `tests/test_bot.py` - Основные тесты бота (запуск, хэндлеры, интеграция)
- `tests/test_media_handlers.py` - Тесты обработки медиа-контента
- `tests/test_error_handling.py` - Тесты обработки ошибок и граничных случаев
- `tests/test_utils.py` - Тесты утилит и вспомогательных функций
- `tests/test_keyboards_and_filters.py` - Тесты клавиатур и фильтров
- `tests/test_db.py` - Тесты базы данных
- `tests/conftest.py` - Общие фикстуры и конфигурация
## Установка зависимостей
```bash
make install
```
## Запуск тестов
### Все тесты
```bash
make test
```
### Отдельные категории тестов
```bash
# Тесты базы данных
make test-db
# Тесты бота (запуск и хэндлеры)
make test-bot
# Тесты обработки медиа
make test-media
# Тесты обработки ошибок
make test-errors
# Тесты утилит
make test-utils
# Тесты клавиатур и фильтров
make test-keyboards
```
### Тесты с покрытием
```bash
# Покрытие с выводом в терминал
make test-coverage
# Покрытие с HTML отчетом
make test-html
```
### Фильтрация тестов
```bash
# Только unit тесты
pytest -m unit
# Только интеграционные тесты
pytest -m integration
# Только асинхронные тесты
pytest -m asyncio
# Исключить медленные тесты
pytest -m "not slow"
# Конкретный файл тестов
pytest tests/test_bot.py
# Конкретный тест
pytest tests/test_bot.py::TestBotStartup::test_bot_initialization
```
## Типы тестов
### Unit тесты
Тестируют отдельные функции и компоненты в изоляции:
- Вспомогательные функции (`get_first_name`, `get_text_message`)
- Утилиты (`BaseDependencyFactory`, `get_message`)
- Фильтры (`ChatTypeFilter`)
- Клавиатуры
### Интеграционные тесты
Тестируют взаимодействие между компонентами:
- Регистрация роутеров в диспетчере
- Обработка сообщений через хэндлеры
- Интеграция с базой данных
### Асинхронные тесты
Тестируют асинхронные функции:
- Хэндлеры сообщений
- Запуск бота
- Обработка медиа-контента
## Моки и фикстуры
### Основные фикстуры
- `mock_message` - Мок сообщения Telegram
- `mock_state` - Мок состояния FSM
- `mock_db` - Мок базы данных
- `mock_bot` - Мок бота
- `mock_dispatcher` - Мок диспетчера
- `mock_factory` - Мок фабрики зависимостей
### Специализированные фикстуры
- `sample_photo_message` - Сообщение с фото
- `sample_video_message` - Сообщение с видео
- `sample_audio_message` - Сообщение с аудио
- `sample_voice_message` - Голосовое сообщение
- `sample_video_note_message` - Видеокружок
- `sample_media_group` - Медиагруппа
- `sample_text_message` - Текстовое сообщение
## Покрытие тестами
### Основные компоненты
- ✅ Запуск бота (`start_bot`)
- ✅ Приватные хэндлеры (`handle_start_message`, `suggest_post`, etc.)
- ✅ Обработка медиа-контента (фото, видео, аудио, голос)
- ✅ Обработка ошибок и исключений
- ✅ Утилиты и вспомогательные функции
- ✅ Клавиатуры и фильтры
- ✅ Фабрика зависимостей
### Тестируемые сценарии
- ✅ Новые пользователи
- ✅ Существующие пользователи
- ✅ Пользователи без username
- ✅ Обработка различных типов контента
- ✅ Медиагруппы
- ✅ Ошибки при получении стикеров
- ✅ Ошибки базы данных
- ✅ Граничные случаи (пустой текст, отсутствие подписей)
## Настройка окружения
### Переменные окружения
Для тестов не требуются реальные токены бота или подключения к базе данных, так как все внешние зависимости замоканы.
### Конфигурация pytest
Настройки pytest находятся в файле `pytest.ini`:
- Автоматический режим asyncio
- Фильтрация предупреждений
- Маркеры для категоризации тестов
## Добавление новых тестов
### Структура теста
```python
@pytest.mark.asyncio
async def test_function_name(mock_message, mock_state, mock_db):
"""Описание теста"""
# Arrange (подготовка)
mock_message.text = "test"
# Act (действие)
result = await function_to_test(mock_message, mock_state)
# Assert (проверка)
assert result is True
mock_message.answer.assert_called_once()
```
### Маркировка тестов
```python
@pytest.mark.unit # Unit тест
@pytest.mark.integration # Интеграционный тест
@pytest.mark.asyncio # Асинхронный тест
@pytest.mark.slow # Медленный тест
```
### Использование фикстур
```python
def test_with_fixtures(mock_message, sample_photo_message, mock_db):
# Используем готовые фикстуры
pass
```
## Отладка тестов
### Подробный вывод
```bash
pytest -v -s
```
### Остановка на первой ошибке
```bash
pytest -x
```
### Вывод полного traceback
```bash
pytest --tb=long
```
### Запуск конкретного теста
```bash
pytest tests/test_bot.py::TestPrivateHandlers::test_handle_start_message_new_user -v
```
## CI/CD интеграция
Тесты могут быть интегрированы в CI/CD pipeline:
```yaml
# Пример для GitHub Actions
- name: Run tests
run: |
make install
make test-coverage
```
## Покрытие кода
Для просмотра покрытия кода:
```bash
make test-html
# Открыть htmlcov/index.html в браузере
```
## Лучшие практики
1. **Изоляция тестов** - каждый тест должен быть независимым
2. **Использование моков** - избегайте реальных внешних зависимостей
3. **Описательные имена** - имена тестов должны описывать что тестируется
4. **Arrange-Act-Assert** - структурируйте тесты по этому паттерну
5. **Фикстуры** - используйте фикстуры для переиспользования кода
6. **Маркировка** - правильно маркируйте тесты для фильтрации
## Устранение неполадок
### Ошибки импорта
Убедитесь, что Python path настроен правильно:
```bash
export PYTHONPATH="${PYTHONPATH}:$(pwd)"
```
### Ошибки asyncio
Для асинхронных тестов используйте маркер `@pytest.mark.asyncio`
### Ошибки моков
Проверьте, что все внешние зависимости замоканы:
```python
with patch('module.function') as mock_func:
# тест
```
### Медленные тесты
Используйте маркер `@pytest.mark.slow` для медленных тестов и исключайте их при необходимости:
```bash
pytest -m "not slow"
```

View File

@@ -7,7 +7,11 @@ from logs.custom_logger import logger
class BotDB: class BotDB:
def __init__(self, current_dir, name): def __init__(self, current_dir, name):
# Формируем правильный путь к базе данных
if name.startswith('database/'):
self.db_file = os.path.join(current_dir, name) self.db_file = os.path.join(current_dir, name)
else:
self.db_file = os.path.join(current_dir, 'database', name)
self.conn = None self.conn = None
self.cursor = None self.cursor = None
self.logger = logger self.logger = logger
@@ -301,6 +305,40 @@ class BotDB:
finally: finally:
self.close() self.close()
def get_user_info_by_id(self, user_id: int):
"""
Возвращает информацию о пользователе из базы данных по его user_id.
Args:
user_id (int): Идентификатор пользователя в Telegram.
Returns:
dict: Словарь с информацией о пользователе (username, full_name).
None: Если пользователь не найден.
Raises:
sqlite3.Error: Если произошла ошибка при выполнении запроса.
"""
try:
self.connect()
self.cursor.execute("SELECT username, full_name FROM our_users WHERE user_id = ?", (user_id,))
result = self.cursor.fetchone()
if result:
user_info = {
'username': result[0],
'full_name': result[1]
}
self.logger.info(f"Информация о пользователе найдена: user_id={user_id}, username={user_info['username']}, full_name={user_info['full_name']}")
return user_info
else:
self.logger.info(f"Пользователь с user_id={user_id} не найден в базе данных.")
return None
except sqlite3.Error as error:
self.logger.error(f"Ошибка при получении информации о пользователе из базы данных: {error}")
raise
finally:
self.close()
def get_all_user_id(self): def get_all_user_id(self):
""" """
Возвращает список всех user_id из базы данных. Возвращает список всех user_id из базы данных.

View File

@@ -7,13 +7,13 @@ from aiogram.fsm.context import FSMContext
from helper_bot.filters.main import ChatTypeFilter from helper_bot.filters.main import ChatTypeFilter
from helper_bot.keyboards.keyboards import get_reply_keyboard_admin, create_keyboard_with_pagination, \ from helper_bot.keyboards.keyboards import get_reply_keyboard_admin, create_keyboard_with_pagination, \
create_keyboard_for_ban_days, create_keyboard_for_approve_ban, create_keyboard_for_ban_reason create_keyboard_for_ban_days, create_keyboard_for_approve_ban, create_keyboard_for_ban_reason
from helper_bot.utils.base_dependency_factory import BaseDependencyFactory from helper_bot.utils.base_dependency_factory import get_global_instance
from helper_bot.utils.helper_func import check_access, add_days_to_date, get_banned_users_buttons, get_banned_users_list from helper_bot.utils.helper_func import check_access, add_days_to_date, get_banned_users_buttons, get_banned_users_list
from logs.custom_logger import logger from logs.custom_logger import logger
admin_router = Router() admin_router = Router()
bdf = BaseDependencyFactory() bdf = get_global_instance()
GROUP_FOR_POST = bdf.settings['Telegram']['group_for_posts'] GROUP_FOR_POST = bdf.settings['Telegram']['group_for_posts']
GROUP_FOR_MESSAGE = bdf.settings['Telegram']['group_for_message'] GROUP_FOR_MESSAGE = bdf.settings['Telegram']['group_for_message']
MAIN_PUBLIC = bdf.settings['Telegram']['main_public'] MAIN_PUBLIC = bdf.settings['Telegram']['main_public']
@@ -32,7 +32,7 @@ BotDB = bdf.get_db()
) )
async def admin_panel(message: types.Message, state: FSMContext): async def admin_panel(message: types.Message, state: FSMContext):
try: try:
if check_access(message.from_user.id): if check_access(message.from_user.id, BotDB):
await state.set_state("ADMIN") await state.set_state("ADMIN")
logger.info(f"Запуск админ панели для пользователя: {message.from_user.id}") logger.info(f"Запуск админ панели для пользователя: {message.from_user.id}")
markup = get_reply_keyboard_admin() markup = get_reply_keyboard_admin()
@@ -70,14 +70,35 @@ async def ban_by_nickname(message: types.Message, state: FSMContext):
await state.set_state('PRE_BAN') await state.set_state('PRE_BAN')
@admin_router.message(
ChatTypeFilter(chat_type=["private"]),
StateFilter("ADMIN"),
F.text == 'Бан по ID'
)
async def ban_by_id(message: types.Message, state: FSMContext):
await message.answer('Пришли мне ID блокируемого пользователя')
await state.set_state('PRE_BAN_ID')
@admin_router.message(
ChatTypeFilter(chat_type=["private"]),
StateFilter("ADMIN"),
F.text == 'Тестовый бан'
)
async def ban_by_forward(message: types.Message, state: FSMContext):
await message.answer('Перешлите мне сообщение от пользователя, которого хотите заблокировать')
await state.set_state('PRE_BAN_FORWARD')
@admin_router.message( @admin_router.message(
ChatTypeFilter(chat_type=["private"]), ChatTypeFilter(chat_type=["private"]),
F.text == 'Отменить' F.text == 'Отменить'
) )
async def decline_ban(message: types.Message, state: FSMContext): async def decline_ban(message: types.Message, state: FSMContext):
current_state = await state.get_state()
await state.set_data({}) await state.set_data({})
await state.set_state("ADMIN") await state.set_state("ADMIN")
logger.info(f"Отмена процедуры блокировки") logger.info(f"Отмена процедуры блокировки из состояния: {current_state}")
markup = get_reply_keyboard_admin() markup = get_reply_keyboard_admin()
await message.answer('Вернулись в меню', reply_markup=markup) await message.answer('Вернулись в меню', reply_markup=markup)
@@ -102,6 +123,110 @@ async def ban_by_nickname_step_2(message: types.Message, state: FSMContext):
await state.set_state('BAN_2') await state.set_state('BAN_2')
@admin_router.message(
ChatTypeFilter(chat_type=["private"]),
StateFilter("PRE_BAN_ID")
)
async def ban_by_id_step_2(message: types.Message, state: FSMContext):
try:
user_id = int(message.text)
logger.info(f"Функция ban_by_id_step_2. Получен ID пользователя: {user_id}")
# Проверяем, существует ли пользователь в базе
user_info = BotDB.get_user_info_by_id(user_id)
if not user_info:
await message.answer(f"Пользователь с ID {user_id} не найден в базе данных.")
await state.set_state('ADMIN')
markup = get_reply_keyboard_admin()
await message.answer('Вернулись в меню', reply_markup=markup)
return
user_name = user_info.get('username', 'Неизвестно')
full_name = user_info.get('full_name', 'Неизвестно')
await state.update_data(user_id=user_id, user_name=user_name, message_for_user=None,
date_to_unban=None)
markup = create_keyboard_for_ban_reason()
await message.answer(
text=f"<b>Выбран пользователь:\nid:</b> {user_id}\n<b>username:</b> {user_name}\n"
f"Имя:{full_name}\nВыбери причину бана из списка или напиши ее в чат",
reply_markup=markup)
await state.set_state('BAN_2')
except ValueError:
await message.answer("Пожалуйста, введите корректный числовой ID пользователя.")
await state.set_state('ADMIN')
markup = get_reply_keyboard_admin()
await message.answer('Вернулись в меню', reply_markup=markup)
@admin_router.message(
ChatTypeFilter(chat_type=["private"]),
StateFilter("PRE_BAN_FORWARD"),
F.forward_from
)
async def ban_by_forward_step_2(message: types.Message, state: FSMContext):
"""Обработчик пересланных сообщений для бана пользователя"""
try:
# Получаем информацию о пользователе из пересланного сообщения
forwarded_user = message.forward_from
if not forwarded_user:
await message.answer("Не удалось получить информацию о пользователе из пересланного сообщения. Возможно, пользователь скрыл возможность пересылки своих сообщений.")
await state.set_state('ADMIN')
markup = get_reply_keyboard_admin()
await message.answer('Вернулись в меню', reply_markup=markup)
return
user_id = forwarded_user.id
user_name = forwarded_user.username or "private_username"
full_name = forwarded_user.full_name or "Неизвестно"
logger.info(f"Функция ban_by_forward_step_2. Получен пользователь из пересланного сообщения: ID={user_id}, username={user_name}, full_name={full_name}")
# Проверяем, существует ли пользователь в базе
user_info = BotDB.get_user_info_by_id(user_id)
if not user_info:
# Если пользователя нет в базе, используем информацию из пересланного сообщения
logger.info(f"Пользователь с ID {user_id} не найден в базе данных, используем данные из пересланного сообщения")
user_name = user_name
full_name = full_name
else:
# Если пользователь есть в базе, используем данные из базы
user_name = user_info.get('username', user_name)
full_name = user_info.get('full_name', full_name)
await state.update_data(user_id=user_id, user_name=user_name, message_for_user=None,
date_to_unban=None)
markup = create_keyboard_for_ban_reason()
await message.answer(
text=f"<b>Выбран пользователь из пересланного сообщения:\nid:</b> {user_id}\n<b>username:</b> {user_name}\n"
f"Имя:{full_name}\nВыбери причину бана из списка или напиши ее в чат",
reply_markup=markup)
await state.set_state('BAN_2')
except Exception as e:
logger.error(f"Ошибка при обработке пересланного сообщения: {e}")
await message.answer("Произошла ошибка при обработке пересланного сообщения.")
await state.set_state('ADMIN')
markup = get_reply_keyboard_admin()
await message.answer('Вернулись в меню', reply_markup=markup)
@admin_router.message(
ChatTypeFilter(chat_type=["private"]),
StateFilter("PRE_BAN_FORWARD")
)
async def ban_by_forward_invalid(message: types.Message, state: FSMContext):
"""Обработчик для случаев, когда сообщение не является пересланным или не содержит информацию о пользователе"""
if message.forward_from_chat:
await message.answer("Пересланное сообщение из канала или группы не содержит информацию о конкретном пользователе. Пожалуйста, перешлите сообщение из приватного чата.")
else:
await message.answer("Пожалуйста, перешлите сообщение от пользователя, которого хотите заблокировать. Обычное сообщение не подходит.")
@admin_router.message( @admin_router.message(
ChatTypeFilter(chat_type=["private"]), ChatTypeFilter(chat_type=["private"]),
StateFilter("ADMIN"), StateFilter("ADMIN"),
@@ -110,8 +235,8 @@ async def ban_by_nickname_step_2(message: types.Message, state: FSMContext):
async def get_banned_users(message): async def get_banned_users(message):
logger.info( logger.info(
f"Попытка получения списка заблокированных пользователей. Текст сообщения: {message.text} Имя автора сообщения: {message.from_user.full_name})") f"Попытка получения списка заблокированных пользователей. Текст сообщения: {message.text} Имя автора сообщения: {message.from_user.full_name})")
message_text = get_banned_users_list(0) message_text = get_banned_users_list(0, BotDB)
buttons_list = get_banned_users_buttons() buttons_list = get_banned_users_buttons(BotDB)
if buttons_list: if buttons_list:
k = create_keyboard_with_pagination(1, len(buttons_list), buttons_list, 'unlock') k = create_keyboard_with_pagination(1, len(buttons_list), buttons_list, 'unlock')
await message.answer(text=message_text, reply_markup=k) await message.answer(text=message_text, reply_markup=k)

View File

@@ -7,7 +7,7 @@ from aiogram.types import CallbackQuery
from helper_bot.keyboards.keyboards import create_keyboard_with_pagination, get_reply_keyboard_admin, \ from helper_bot.keyboards.keyboards import create_keyboard_with_pagination, get_reply_keyboard_admin, \
create_keyboard_for_ban_reason create_keyboard_for_ban_reason
from helper_bot.utils.base_dependency_factory import BaseDependencyFactory from helper_bot.utils.base_dependency_factory import get_global_instance
from helper_bot.utils.helper_func import send_text_message, send_photo_message, get_banned_users_list, \ from helper_bot.utils.helper_func import send_text_message, send_photo_message, get_banned_users_list, \
get_banned_users_buttons, delete_user_blacklist, send_media_group_to_channel, \ get_banned_users_buttons, delete_user_blacklist, send_media_group_to_channel, \
send_video_message, send_video_note_message, send_audio_message, send_voice_message send_video_message, send_video_note_message, send_audio_message, send_voice_message
@@ -15,7 +15,7 @@ from logs.custom_logger import logger
callback_router = Router() callback_router = Router()
bdf = BaseDependencyFactory() bdf = get_global_instance()
GROUP_FOR_POST = bdf.settings['Telegram']['group_for_posts'] GROUP_FOR_POST = bdf.settings['Telegram']['group_for_posts']
GROUP_FOR_MESSAGE = bdf.settings['Telegram']['group_for_message'] GROUP_FOR_MESSAGE = bdf.settings['Telegram']['group_for_message']
MAIN_PUBLIC = bdf.settings['Telegram']['main_public'] MAIN_PUBLIC = bdf.settings['Telegram']['main_public']
@@ -237,7 +237,7 @@ async def process_ban_user(call: CallbackQuery, state: FSMContext):
async def process_unlock_user(call: CallbackQuery): async def process_unlock_user(call: CallbackQuery):
user_id = call.data[7:] user_id = call.data[7:]
user_name = BotDB.get_username(user_id=user_id) user_name = BotDB.get_username(user_id=user_id)
delete_user_blacklist(user_id) delete_user_blacklist(user_id, BotDB)
logger.info(f"Разблокирован пользователь с ID: {user_id} username:{user_name}") logger.info(f"Разблокирован пользователь с ID: {user_id} username:{user_name}")
username = BotDB.get_username(user_id) username = BotDB.get_username(user_id)
await call.answer(f'Пользователь разблокирован {username}', show_alert=True) await call.answer(f'Пользователь разблокирован {username}', show_alert=True)
@@ -270,12 +270,12 @@ async def change_page(call: CallbackQuery):
reply_markup=keyboard) reply_markup=keyboard)
else: else:
# Готовим сообщения # Готовим сообщения
message_user = get_banned_users_list(int(page_number) * 7 - 7) message_user = get_banned_users_list(int(page_number) * 7 - 7, BotDB)
await call.bot.edit_message_text(chat_id=call.message.chat.id, message_id=call.message.message_id, await call.bot.edit_message_text(chat_id=call.message.chat.id, message_id=call.message.message_id,
text=message_user) text=message_user)
# Готовим клавиатуру # Готовим клавиатуру
buttons = get_banned_users_buttons() buttons = get_banned_users_buttons(BotDB)
keyboard = create_keyboard_with_pagination(int(call.data[5:]), len(buttons), buttons, 'unlock') keyboard = create_keyboard_with_pagination(int(call.data[5:]), len(buttons), buttons, 'unlock')
await call.bot.edit_message_reply_markup(chat_id=call.message.chat.id, message_id=call.message.message_id, await call.bot.edit_message_reply_markup(chat_id=call.message.chat.id, message_id=call.message.message_id,
reply_markup=keyboard) reply_markup=keyboard)

View File

@@ -3,13 +3,13 @@ from aiogram.fsm.context import FSMContext
from helper_bot.filters.main import ChatTypeFilter from helper_bot.filters.main import ChatTypeFilter
from helper_bot.keyboards.keyboards import get_reply_keyboard_leave_chat from helper_bot.keyboards.keyboards import get_reply_keyboard_leave_chat
from helper_bot.utils.base_dependency_factory import BaseDependencyFactory from helper_bot.utils.base_dependency_factory import get_global_instance
from helper_bot.utils.helper_func import send_text_message from helper_bot.utils.helper_func import send_text_message
from logs.custom_logger import logger from logs.custom_logger import logger
group_router = Router() group_router = Router()
bdf = BaseDependencyFactory() bdf = get_global_instance()
GROUP_FOR_POST = bdf.settings['Telegram']['group_for_posts'] GROUP_FOR_POST = bdf.settings['Telegram']['group_for_posts']
GROUP_FOR_MESSAGE = bdf.settings['Telegram']['group_for_message'] GROUP_FOR_MESSAGE = bdf.settings['Telegram']['group_for_message']
MAIN_PUBLIC = bdf.settings['Telegram']['main_public'] MAIN_PUBLIC = bdf.settings['Telegram']['main_public']

View File

@@ -15,7 +15,7 @@ from helper_bot.keyboards.keyboards import get_reply_keyboard_leave_chat
from helper_bot.middlewares.album_middleware import AlbumMiddleware from helper_bot.middlewares.album_middleware import AlbumMiddleware
from helper_bot.middlewares.blacklist_middleware import BlacklistMiddleware from helper_bot.middlewares.blacklist_middleware import BlacklistMiddleware
from helper_bot.utils import messages from helper_bot.utils import messages
from helper_bot.utils.base_dependency_factory import BaseDependencyFactory from helper_bot.utils.base_dependency_factory import get_global_instance
from helper_bot.utils.helper_func import get_first_name, get_text_message, send_text_message, send_photo_message, \ from helper_bot.utils.helper_func import get_first_name, get_text_message, send_text_message, send_photo_message, \
send_media_group_message_to_private_chat, prepare_media_group_from_middlewares, check_username_and_full_name, \ send_media_group_message_to_private_chat, prepare_media_group_from_middlewares, check_username_and_full_name, \
send_video_message, send_video_note_message, send_audio_message, send_voice_message, add_in_db_media send_video_message, send_video_note_message, send_audio_message, send_voice_message, add_in_db_media
@@ -26,7 +26,7 @@ private_router = Router()
private_router.message.middleware(AlbumMiddleware()) private_router.message.middleware(AlbumMiddleware())
private_router.message.middleware(BlacklistMiddleware()) private_router.message.middleware(BlacklistMiddleware())
bdf = BaseDependencyFactory() bdf = get_global_instance()
GROUP_FOR_POST = bdf.settings['Telegram']['group_for_posts'] GROUP_FOR_POST = bdf.settings['Telegram']['group_for_posts']
GROUP_FOR_MESSAGE = bdf.settings['Telegram']['group_for_message'] GROUP_FOR_MESSAGE = bdf.settings['Telegram']['group_for_message']
MAIN_PUBLIC = bdf.settings['Telegram']['main_public'] MAIN_PUBLIC = bdf.settings['Telegram']['main_public']
@@ -56,13 +56,22 @@ async def handle_start_message(message: types.Message, state: FSMContext):
is_bot = message.from_user.is_bot is_bot = message.from_user.is_bot
language_code = message.from_user.language_code language_code = message.from_user.language_code
user_id = message.from_user.id user_id = message.from_user.id
# Проверяем наличие username для логирования
if not username:
await message.bot.send_message(chat_id=GROUP_FOR_LOGS,
text=f'Пользователь {user_id} ({full_name}) обратился к боту без username')
logger.warning(f"Пользователь {user_id} ({full_name}) обратился к боту без username")
# Устанавливаем значение по умолчанию для username
username = "private_username"
current_date = datetime.now() current_date = datetime.now()
date = current_date.strftime("%Y-%m-%d %H:%M:%S") date = current_date.strftime("%Y-%m-%d %H:%M:%S")
if not BotDB.user_exists(user_id): if not BotDB.user_exists(user_id):
BotDB.add_new_user_in_db(user_id, first_name, full_name, username, is_bot, language_code, date, BotDB.add_new_user_in_db(user_id, first_name, full_name, username, is_bot, language_code, date,
date) date)
else: else:
is_need_update = check_username_and_full_name(user_id, username, full_name) is_need_update = check_username_and_full_name(user_id, username, full_name, BotDB)
if is_need_update: if is_need_update:
BotDB.update_username_and_full_name(user_id, username, full_name) BotDB.update_username_and_full_name(user_id, username, full_name)
await message.answer( await message.answer(
@@ -96,6 +105,30 @@ async def handle_start_message(message: types.Message, state: FSMContext):
f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}") f"Произошла ошибка: {str(e)}\n\nTraceback:\n{traceback.format_exc()}")
@private_router.message(
ChatTypeFilter(chat_type=["private"]),
Command("restart")
)
async def restart_function(message: types.Message, state: FSMContext):
await message.forward(chat_id=GROUP_FOR_LOGS)
full_name = message.from_user.full_name
username = message.from_user.username
user_id = message.from_user.id
# Проверяем наличие username для логирования
if not username:
await message.bot.send_message(chat_id=GROUP_FOR_LOGS,
text=f'Пользователь {user_id} ({full_name}) обратился к боту без username')
logger.warning(f"Пользователь {user_id} ({full_name}) обратился к боту без username")
# Устанавливаем значение по умолчанию для username
username = "private_username"
markup = get_reply_keyboard(BotDB, message.from_user.id)
await message.answer(text='Я перезапущен!',
reply_markup=markup)
await state.set_state('START')
@private_router.message( @private_router.message(
StateFilter("START"), StateFilter("START"),
ChatTypeFilter(chat_type=["private"]), ChatTypeFilter(chat_type=["private"]),
@@ -209,7 +242,7 @@ async def suggest_router(message: types.Message, state: FSMContext, album: list
message.photo[-1].file_id, post_caption, markup) message.photo[-1].file_id, post_caption, markup)
# Записываем в базу пост и контент # Записываем в базу пост и контент
BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id) BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id)
await add_in_db_media(sent_message) await add_in_db_media(sent_message, BotDB)
# Отправляем юзеру ответ и возвращаем его в меню # Отправляем юзеру ответ и возвращаем его в меню
markup_for_user = get_reply_keyboard(BotDB, message.from_user.id) markup_for_user = get_reply_keyboard(BotDB, message.from_user.id)
@@ -231,7 +264,7 @@ async def suggest_router(message: types.Message, state: FSMContext, album: list
# Записываем в базу пост и контент # Записываем в базу пост и контент
BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id) BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id)
await add_in_db_media(sent_message) await add_in_db_media(sent_message, BotDB)
# Записываем в базу пост и контент # Записываем в базу пост и контент
BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id) BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id)
@@ -252,7 +285,7 @@ async def suggest_router(message: types.Message, state: FSMContext, album: list
# Записываем в базу пост и контент # Записываем в базу пост и контент
BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id) BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id)
await add_in_db_media(sent_message) await add_in_db_media(sent_message, BotDB)
# Отправляем юзеру ответ и возвращаем его в меню # Отправляем юзеру ответ и возвращаем его в меню
markup_for_user = get_reply_keyboard(BotDB, message.from_user.id) markup_for_user = get_reply_keyboard(BotDB, message.from_user.id)
@@ -274,7 +307,7 @@ async def suggest_router(message: types.Message, state: FSMContext, album: list
# Записываем в базу пост и контент # Записываем в базу пост и контент
BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id) BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id)
await add_in_db_media(sent_message) await add_in_db_media(sent_message, BotDB)
# Отправляем юзеру ответ и возвращаем его в меню # Отправляем юзеру ответ и возвращаем его в меню
markup_for_user = get_reply_keyboard(BotDB, message.from_user.id) markup_for_user = get_reply_keyboard(BotDB, message.from_user.id)
@@ -291,7 +324,7 @@ async def suggest_router(message: types.Message, state: FSMContext, album: list
# Записываем в базу пост и контент # Записываем в базу пост и контент
BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id) BotDB.add_post_in_db(sent_message.message_id, sent_message.caption, message.from_user.id)
await add_in_db_media(sent_message) await add_in_db_media(sent_message, BotDB)
# Отправляем юзеру ответ и возвращаем его в меню # Отправляем юзеру ответ и возвращаем его в меню
markup_for_user = get_reply_keyboard(BotDB, message.from_user.id) markup_for_user = get_reply_keyboard(BotDB, message.from_user.id)
@@ -313,7 +346,7 @@ async def suggest_router(message: types.Message, state: FSMContext, album: list
# Отправляем медиагруппу в секретный чат # Отправляем медиагруппу в секретный чат
media_group_message_id = await send_media_group_message_to_private_chat(GROUP_FOR_POST, message, media_group_message_id = await send_media_group_message_to_private_chat(GROUP_FOR_POST, message,
media_group) media_group, BotDB)
sleep(0.2) sleep(0.2)
# Получаем клавиатуру и отправляем еще одно текстовое сообщение с кнопками # Получаем клавиатуру и отправляем еще одно текстовое сообщение с кнопками

View File

@@ -36,6 +36,8 @@ def get_reply_keyboard_admin():
builder = ReplyKeyboardBuilder() builder = ReplyKeyboardBuilder()
builder.add(types.KeyboardButton(text="Бан (Список)")) builder.add(types.KeyboardButton(text="Бан (Список)"))
builder.add(types.KeyboardButton(text="Бан по нику")) builder.add(types.KeyboardButton(text="Бан по нику"))
builder.add(types.KeyboardButton(text="Бан по ID"))
builder.add(types.KeyboardButton(text="Тестовый бан"))
builder.add(types.KeyboardButton(text="Разбан (список)")) builder.add(types.KeyboardButton(text="Разбан (список)"))
builder.add(types.KeyboardButton(text="Вернуться в бота")) builder.add(types.KeyboardButton(text="Вернуться в бота"))
markup = builder.as_markup(resize_keyboard=True, one_time_keyboard=True) markup = builder.as_markup(resize_keyboard=True, one_time_keyboard=True)

View File

@@ -1,10 +1,10 @@
from typing import Dict, Any from typing import Dict, Any
from aiogram import BaseMiddleware, types from aiogram import BaseMiddleware, types
from helper_bot.utils.base_dependency_factory import BaseDependencyFactory from helper_bot.utils.base_dependency_factory import get_global_instance
from logs.custom_logger import logger from logs.custom_logger import logger
bdf = BaseDependencyFactory() bdf = get_global_instance()
BotDB = bdf.get_db() BotDB = bdf.get_db()

View File

@@ -14,7 +14,7 @@ class BaseDependencyFactory:
self.config = configparser.ConfigParser() self.config = configparser.ConfigParser()
self.config.read(config_path) self.config.read(config_path)
self.settings = {} self.settings = {}
self.database = BotDB(current_dir, 'database/tg-bot-database') self.database = BotDB(current_dir, 'tg-bot-database.db')
for section in self.config.sections(): for section in self.config.sections():
self.settings[section] = {} self.settings[section] = {}
@@ -33,3 +33,14 @@ class BaseDependencyFactory:
def get_db(self) -> BotDB: def get_db(self) -> BotDB:
"""Возвращает подключение к базе данных.""" """Возвращает подключение к базе данных."""
return self.database return self.database
# Создаем единый экземпляр для всего приложения
_global_instance = None
def get_global_instance():
"""Возвращает глобальный экземпляр BaseDependencyFactory."""
global _global_instance
if _global_instance is None:
_global_instance = BaseDependencyFactory()
return _global_instance

View File

@@ -8,34 +8,36 @@ from aiogram.types import InputMediaPhoto, FSInputFile, InputMediaVideo, InputMe
from helper_bot.utils.base_dependency_factory import BaseDependencyFactory from helper_bot.utils.base_dependency_factory import BaseDependencyFactory
from logs.custom_logger import logger from logs.custom_logger import logger
bdf = BaseDependencyFactory()
BotDB = bdf.get_db()
def get_first_name(message: types.Message) -> str: def get_first_name(message: types.Message) -> str:
first_name = html.escape(message.from_user.first_name) first_name = html.escape(message.from_user.first_name)
return first_name return first_name
def get_text_message(post_text: str, first_name: str, username: str): def get_text_message(post_text: str, first_name: str, username: str = None):
""" """
Форматирует текст сообщения для публикации в зависимости от наличия ключевых слов "анон" и "неанон". Форматирует текст сообщения для публикации в зависимости от наличия ключевых слов "анон" и "неанон".
Args: Args:
post_text: Текст сообщения post_text: Текст сообщения
first_name: Имя автора поста first_name: Имя автора поста
username: Юзернейм автора поста username: Юзернейм автора поста (может быть None)
Returns: Returns:
str: - Сформированный текст сообщения. str: - Сформированный текст сообщения.
""" """
# Формируем строку с информацией об авторе
if username:
author_info = f"{first_name} @{username}"
else:
author_info = f"{first_name} (Ник не указан)"
if "неанон" in post_text or "не анон" in post_text: if "неанон" in post_text or "не анон" in post_text:
return f'Пост из ТГ:\n{post_text}\n\nАвтор поста: {first_name} @{username}' return f'Пост из ТГ:\n{post_text}\n\nАвтор поста: {author_info}'
elif "анон" in post_text: elif "анон" in post_text:
return f'Пост из ТГ:\n{post_text}\n\nПост опубликован анонимно' return f'Пост из ТГ:\n{post_text}\n\nПост опубликован анонимно'
else: else:
return f'Пост из ТГ:\n{post_text}\n\nАвтор поста: {first_name} @{username}' return f'Пост из ТГ:\n{post_text}\n\nАвтор поста: {author_info}'
async def download_file(message: types.Message, file_id: str): async def download_file(message: types.Message, file_id: str):
@@ -112,12 +114,13 @@ async def prepare_media_group_from_middlewares(album, post_caption: str = ''):
return media_group # Возвращаем MediaGroup return media_group # Возвращаем MediaGroup
async def add_in_db_media_mediagroup(sent_message): async def add_in_db_media_mediagroup(sent_message, bot_db):
""" """
Идентификатор медиа-группы Идентификатор медиа-группы
Args: Args:
sent_message: sent_message объект из Telegram API sent_message: sent_message объект из Telegram API
bot_db: Экземпляр базы данных
Returns: Returns:
Список InputFile (FSInputFile). Список InputFile (FSInputFile).
@@ -127,20 +130,21 @@ async def add_in_db_media_mediagroup(sent_message):
if message.photo: if message.photo:
file_id = message.photo[-1].file_id file_id = message.photo[-1].file_id
file_path = await download_file(message, file_id=file_id) file_path = await download_file(message, file_id=file_id)
BotDB.add_post_content_in_db(media_group_message_id, message.message_id, file_path, 'photo') bot_db.add_post_content_in_db(media_group_message_id, message.message_id, file_path, 'photo')
elif message.video: elif message.video:
file_id = message.video.file_id file_id = message.video.file_id
file_path = await download_file(message, file_id=file_id) file_path = await download_file(message, file_id=file_id)
BotDB.add_post_content_in_db(media_group_message_id, message.message_id, file_path, 'video') bot_db.add_post_content_in_db(media_group_message_id, message.message_id, file_path, 'video')
else: else:
# Если нет фото, видео или аудио, или другой контент, пропускаем сообщение # Если нет фото, видео или аудио, или другой контент, пропускаем сообщение
continue continue
async def add_in_db_media(sent_message): async def add_in_db_media(sent_message, bot_db):
""" """
Args: Args:
sent_message: sent_message объект из Telegram API sent_message: sent_message объект из Telegram API
bot_db: Экземпляр базы данных
Returns: Returns:
Список InputFile (FSInputFile). Список InputFile (FSInputFile).
@@ -148,33 +152,33 @@ async def add_in_db_media(sent_message):
if sent_message.photo: if sent_message.photo:
file_id = sent_message.photo[-1].file_id file_id = sent_message.photo[-1].file_id
file_path = await download_file(sent_message, file_id=file_id) file_path = await download_file(sent_message, file_id=file_id)
BotDB.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'photo') bot_db.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'photo')
elif sent_message.video: elif sent_message.video:
file_id = sent_message.video.file_id file_id = sent_message.video.file_id
file_path = await download_file(sent_message, file_id=file_id) file_path = await download_file(sent_message, file_id=file_id)
BotDB.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'video') bot_db.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'video')
elif sent_message.voice: elif sent_message.voice:
file_id = sent_message.voice.file_id file_id = sent_message.voice.file_id
file_path = await download_file(sent_message, file_id=file_id) file_path = await download_file(sent_message, file_id=file_id)
BotDB.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'voice') bot_db.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'voice')
elif sent_message.audio: elif sent_message.audio:
file_id = sent_message.audio.file_id file_id = sent_message.audio.file_id
file_path = await download_file(sent_message, file_id=file_id) file_path = await download_file(sent_message, file_id=file_id)
BotDB.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'audio') bot_db.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'audio')
elif sent_message.video_note: elif sent_message.video_note:
file_id = sent_message.video_note.file_id file_id = sent_message.video_note.file_id
file_path = await download_file(sent_message, file_id=file_id) file_path = await download_file(sent_message, file_id=file_id)
BotDB.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'video_note') bot_db.add_post_content_in_db(sent_message.message_id, sent_message.message_id, file_path, 'video_note')
async def send_media_group_message_to_private_chat(chat_id: int, message: types.Message, async def send_media_group_message_to_private_chat(chat_id: int, message: types.Message,
media_group: list[InputMediaPhoto]): media_group: list[InputMediaPhoto], bot_db):
sent_message = await message.bot.send_media_group( sent_message = await message.bot.send_media_group(
chat_id=chat_id, chat_id=chat_id,
media=media_group, media=media_group,
) )
BotDB.add_post_in_db(sent_message[-1].message_id, sent_message[-1].caption, message.from_user.id) bot_db.add_post_in_db(sent_message[-1].message_id, sent_message[-1].caption, message.from_user.id)
await add_in_db_media_mediagroup(sent_message) await add_in_db_media_mediagroup(sent_message, bot_db)
message_id = sent_message[-1].message_id message_id = sent_message[-1].message_id
return message_id return message_id
@@ -313,9 +317,9 @@ async def send_voice_message(chat_id, message: types.Message, voice: str,
return sent_message return sent_message
def check_access(user_id: int): def check_access(user_id: int, bot_db):
"""Проверка прав на совершение действий""" """Проверка прав на совершение действий"""
return BotDB.is_admin(user_id) return bot_db.is_admin(user_id)
def add_days_to_date(days: int): def add_days_to_date(days: int):
@@ -326,18 +330,19 @@ def add_days_to_date(days: int):
return formatted_date return formatted_date
def get_banned_users_list(offset: int): def get_banned_users_list(offset: int, bot_db):
""" """
Возвращает сообщение со списком пользователей и словарь с ником + идентификатором Возвращает сообщение со списком пользователей и словарь с ником + идентификатором
Args: Args:
offset: отступ для запроса в базу данных offset: отступ для запроса в базу данных
bot_db: Экземпляр базы данных
Returns: Returns:
message - текст сообщения message - текст сообщения
user_ids - лист кортежей [(user_name: user_id)] user_ids - лист кортежей [(user_name: user_id)]
""" """
users = BotDB.get_banned_users_from_db_with_limits(limit=7, offset=offset) users = bot_db.get_banned_users_from_db_with_limits(limit=7, offset=offset)
message = "Список заблокированных пользователей:\n" message = "Список заблокированных пользователей:\n"
for user in users: for user in users:
@@ -347,18 +352,18 @@ def get_banned_users_list(offset: int):
return message return message
def get_banned_users_buttons(): def get_banned_users_buttons(bot_db):
""" """
Возвращает сообщение со списком пользователей и словарь с ником + идентификатором Возвращает сообщение со списком пользователей и словарь с ником + идентификатором
Args: Args:
offset: отступ для запроса в базу данных bot_db: Экземпляр базы данных
Returns: Returns:
message - текст сообщения message - текст сообщения
user_ids - лист кортежей [(user_name: user_id)] user_ids - лист кортежей [(user_name: user_id)]
""" """
users = BotDB.get_banned_users_from_db() users = bot_db.get_banned_users_from_db()
user_ids = [] user_ids = []
for user in users: for user in users:
@@ -366,12 +371,12 @@ def get_banned_users_buttons():
return user_ids return user_ids
def delete_user_blacklist(user_id: int): def delete_user_blacklist(user_id: int, bot_db):
return BotDB.delete_user_blacklist(user_id=user_id) return bot_db.delete_user_blacklist(user_id=user_id)
def check_username_and_full_name(user_id: int, username: str, full_name: str): def check_username_and_full_name(user_id: int, username: str, full_name: str, bot_db):
username_db, full_name_db = BotDB.get_username_and_full_name(user_id=user_id) username_db, full_name_db = bot_db.get_username_and_full_name(user_id=user_id)
return username != username_db or full_name != full_name_db return username != username_db or full_name != full_name_db

View File

@@ -8,6 +8,8 @@ class StateUser(StatesGroup):
CHAT = State() CHAT = State()
PRE_CHAT = State() PRE_CHAT = State()
PRE_BAN = State() PRE_BAN = State()
PRE_BAN_ID = State()
PRE_BAN_FORWARD = State()
BAN_2 = State() BAN_2 = State()
BAN_3 = State() BAN_3 = State()
BAN_4 = State() BAN_4 = State()

View File

@@ -8,7 +8,7 @@ current_dir = os.path.dirname(__file__)
# Переходим на уровень выше # Переходим на уровень выше
parent_dir = os.path.dirname(current_dir) parent_dir = os.path.dirname(current_dir)
BotDB = BotDB(parent_dir, 'database/tg-bot-database') BotDB = BotDB(parent_dir, 'tg-bot-database.db')
def get_filename(): def get_filename():

View File

@@ -8,7 +8,7 @@ current_dir = os.path.dirname(__file__)
# Переходим на уровень выше # Переходим на уровень выше
parent_dir = os.path.dirname(current_dir) parent_dir = os.path.dirname(current_dir)
BotDB = BotDB(parent_dir, 'database/tg-bot-database') BotDB = BotDB(parent_dir, 'tg-bot-database.db')
def get_filename(): def get_filename():

View File

@@ -8,7 +8,7 @@ current_dir = os.path.dirname(__file__)
# Переходим на уровень выше # Переходим на уровень выше
parent_dir = os.path.dirname(current_dir) parent_dir = os.path.dirname(current_dir)
BotDB = BotDB(parent_dir, 'database/tg-bot-database') BotDB = BotDB(parent_dir, 'tg-bot-database.db')
def get_filename(): def get_filename():

View File

@@ -1,8 +1,19 @@
[pytest] [tool:pytest]
pythonpath = .
python_files = test_*.py *_test.py
python_functions = test_*
testpaths = tests testpaths = tests
python_files = test_*.py
[report] python_classes = Test*
omit = *myenv/*, custom_logger.py, *venv/*, tests/* python_functions = test_*
addopts =
-v
--tb=short
--strict-markers
--disable-warnings
--asyncio-mode=auto
markers =
asyncio: marks tests as async (deselect with '-m "not asyncio"')
slow: marks tests as slow (deselect with '-m "not slow"')
integration: marks tests as integration tests
unit: marks tests as unit tests
filterwarnings =
ignore::DeprecationWarning
ignore::PendingDeprecationWarning

View File

@@ -1,7 +1,7 @@
import asyncio import asyncio
from helper_bot.main import start_bot from helper_bot.main import start_bot
from helper_bot.utils.base_dependency_factory import BaseDependencyFactory from helper_bot.utils.base_dependency_factory import get_global_instance
if __name__ == '__main__': if __name__ == '__main__':
asyncio.run(start_bot(BaseDependencyFactory())) asyncio.run(start_bot(get_global_instance()))

234
tests/conftest.py Normal file
View File

@@ -0,0 +1,234 @@
import pytest
import asyncio
import os
import sys
from unittest.mock import Mock, AsyncMock, patch
from aiogram.types import Message, User, Chat
from aiogram.fsm.context import FSMContext
from database.db import BotDB
# Импортируем моки в самом начале
import tests.mocks
@pytest.fixture(scope="session")
def event_loop():
"""Создает event loop для асинхронных тестов"""
loop = asyncio.get_event_loop_policy().new_event_loop()
yield loop
loop.close()
@pytest.fixture
def mock_message():
"""Создает базовый мок сообщения для тестов"""
message = Mock(spec=Message)
message.from_user = Mock(spec=User)
message.from_user.id = 123456
message.from_user.full_name = "Test User"
message.from_user.username = "testuser"
message.from_user.first_name = "Test"
message.from_user.is_bot = False
message.from_user.language_code = "ru"
message.chat = Mock(spec=Chat)
message.chat.id = 123456
message.chat.type = "private"
message.message_id = 1
message.text = "/start"
message.forward = AsyncMock()
message.answer = AsyncMock()
message.answer_sticker = AsyncMock()
message.bot.send_message = AsyncMock()
return message
@pytest.fixture
def mock_state():
"""Создает мок состояния FSM для тестов"""
state = Mock(spec=FSMContext)
state.set_state = AsyncMock()
state.get_state = AsyncMock(return_value="START")
return state
@pytest.fixture
def mock_db():
"""Создает мок базы данных для тестов"""
db = Mock(spec=BotDB)
db.user_exists = Mock(return_value=False)
db.add_new_user_in_db = Mock()
db.update_date_for_user = Mock()
db.update_username_and_full_name = Mock()
db.add_post_in_db = Mock()
db.update_info_about_stickers = Mock()
db.add_new_message_in_db = Mock()
db.get_info_about_stickers = Mock(return_value=False)
db.get_username_and_full_name = Mock(return_value=("testuser", "Test User"))
return db
@pytest.fixture
def mock_bot():
"""Создает мок бота для тестов"""
bot = AsyncMock()
bot.send_message = AsyncMock()
bot.delete_webhook = AsyncMock()
return bot
@pytest.fixture
def mock_dispatcher():
"""Создает мок диспетчера для тестов"""
dispatcher = AsyncMock()
dispatcher.include_routers = Mock()
dispatcher.start_polling = AsyncMock()
return dispatcher
@pytest.fixture
def test_settings():
"""Возвращает тестовые настройки"""
return {
'Telegram': {
'bot_token': 'test_token_123',
'preview_link': False,
'group_for_posts': '-1001234567890',
'group_for_message': '-1001234567891',
'main_public': '-1001234567892',
'group_for_logs': '-1001234567893',
'important_logs': '-1001234567894'
},
'Settings': {
'logs': True,
'test': False
}
}
@pytest.fixture
def mock_factory(test_settings, mock_db):
"""Создает мок фабрики зависимостей"""
factory = Mock()
factory.settings = test_settings
factory.get_db = Mock(return_value=mock_db)
factory.database = mock_db
return factory
@pytest.fixture
def sample_photo_message(mock_message):
"""Создает сообщение с фото для тестов"""
mock_message.content_type = 'photo'
mock_message.caption = 'Тестовое фото'
mock_message.media_group_id = None
mock_message.photo = [Mock()]
mock_message.photo[-1].file_id = 'photo_file_id'
return mock_message
@pytest.fixture
def sample_video_message(mock_message):
"""Создает сообщение с видео для тестов"""
mock_message.content_type = 'video'
mock_message.caption = 'Тестовое видео'
mock_message.media_group_id = None
mock_message.video = Mock()
mock_message.video.file_id = 'video_file_id'
return mock_message
@pytest.fixture
def sample_audio_message(mock_message):
"""Создает сообщение с аудио для тестов"""
mock_message.content_type = 'audio'
mock_message.caption = 'Тестовое аудио'
mock_message.media_group_id = None
mock_message.audio = Mock()
mock_message.audio.file_id = 'audio_file_id'
return mock_message
@pytest.fixture
def sample_voice_message(mock_message):
"""Создает голосовое сообщение для тестов"""
mock_message.content_type = 'voice'
mock_message.media_group_id = None
mock_message.voice = Mock()
mock_message.voice.file_id = 'voice_file_id'
return mock_message
@pytest.fixture
def sample_video_note_message(mock_message):
"""Создает видеокружок для тестов"""
mock_message.content_type = 'video_note'
mock_message.media_group_id = None
mock_message.video_note = Mock()
mock_message.video_note.file_id = 'video_note_file_id'
return mock_message
@pytest.fixture
def sample_media_group(mock_message):
"""Создает медиагруппу для тестов"""
mock_message.media_group_id = 'group_123'
mock_message.content_type = 'photo'
album = [mock_message]
album[0].caption = 'Подпись к медиагруппе'
return album
@pytest.fixture
def sample_text_message(mock_message):
"""Создает текстовое сообщение для тестов"""
mock_message.content_type = 'text'
mock_message.text = 'Тестовое текстовое сообщение'
mock_message.media_group_id = None
return mock_message
@pytest.fixture
def sample_document_message(mock_message):
"""Создает сообщение с документом для тестов"""
mock_message.content_type = 'document'
mock_message.media_group_id = None
return mock_message
# Маркеры для категоризации тестов
def pytest_configure(config):
"""Настройка маркеров pytest"""
config.addinivalue_line(
"markers", "asyncio: mark test as async"
)
config.addinivalue_line(
"markers", "slow: mark test as slow"
)
config.addinivalue_line(
"markers", "integration: mark test as integration test"
)
config.addinivalue_line(
"markers", "unit: mark test as unit test"
)
# Автоматическая маркировка тестов
def pytest_collection_modifyitems(config, items):
"""Автоматически маркирует тесты по их расположению"""
for item in items:
# Маркируем асинхронные тесты
if "async" in item.name or "Async" in item.name:
item.add_marker(pytest.mark.asyncio)
# Маркируем интеграционные тесты
if "integration" in item.name.lower() or "Integration" in str(item.cls):
item.add_marker(pytest.mark.integration)
# Маркируем unit тесты
if "unit" in item.name.lower() or "Unit" in str(item.cls):
item.add_marker(pytest.mark.unit)
# Маркируем медленные тесты
if "slow" in item.name.lower() or "Slow" in str(item.cls):
item.add_marker(pytest.mark.slow)

52
tests/mocks.py Normal file
View File

@@ -0,0 +1,52 @@
"""
Моки для тестового окружения
"""
import sys
import os
from unittest.mock import Mock, patch
# Патчим загрузку настроек до импорта модулей
def setup_test_mocks():
"""Настройка моков для тестов"""
# Мокаем ConfigParser
mock_config = Mock()
def mock_getitem(section):
if section == 'Telegram':
return {
'bot_token': 'test_token_123',
'preview_link': 'False',
'main_public': '@test',
'group_for_posts': '-1001234567890',
'group_for_message': '-1001234567891',
'group_for_logs': '-1001234567893',
'important_logs': '-1001234567894',
'test_channel': '-1001234567895'
}
elif section == 'Settings':
return {
'logs': 'True',
'test': 'False'
}
return {}
# Создаем MagicMock для поддержки __getitem__
mock_config_instance = Mock()
mock_config_instance.sections.return_value = ['Telegram', 'Settings']
mock_config_instance.__getitem__ = Mock(side_effect=mock_getitem)
mock_config.return_value = mock_config_instance
# Применяем патчи
config_patcher = patch('helper_bot.utils.base_dependency_factory.configparser.ConfigParser', mock_config)
config_patcher.start()
# Мокаем BotDB
mock_db = Mock()
db_patcher = patch('helper_bot.utils.base_dependency_factory.BotDB', mock_db)
db_patcher.start()
return config_patcher, db_patcher
# Настраиваем моки при импорте модуля
config_patcher, db_patcher = setup_test_mocks()

339
tests/test_bot.py Normal file
View File

@@ -0,0 +1,339 @@
# Импортируем моки в самом начале
import tests.mocks
import pytest
import asyncio
from unittest.mock import Mock, AsyncMock, patch, MagicMock
from aiogram import Bot, Dispatcher
from aiogram.types import Message, User, Chat, MessageEntity
from aiogram.fsm.context import FSMContext
from aiogram.fsm.storage.memory import MemoryStorage
from helper_bot.main import start_bot
from helper_bot.handlers.private.private_handlers import (
handle_start_message,
restart_function,
suggest_post,
end_message,
suggest_router,
stickers,
connect_with_admin,
resend_message_in_group_for_message
)
from helper_bot.utils.base_dependency_factory import BaseDependencyFactory, get_global_instance
from database.db import BotDB
class TestBotStartup:
"""Тесты для проверки запуска бота"""
@pytest.mark.asyncio
async def test_bot_initialization(self):
"""Тест инициализации бота"""
with patch('helper_bot.main.Bot') as mock_bot_class:
with patch('helper_bot.main.Dispatcher') as mock_dp_class:
with patch('helper_bot.main.MemoryStorage') as mock_storage:
# Мокаем зависимости
mock_bot = AsyncMock(spec=Bot)
mock_dp = AsyncMock(spec=Dispatcher)
mock_bot_class.return_value = mock_bot
mock_dp_class.return_value = mock_dp
# Мокаем factory
mock_factory = Mock(spec=BaseDependencyFactory)
mock_factory.settings = {
'Telegram': {
'bot_token': 'test_token',
'preview_link': False
}
}
# Запускаем бота
await start_bot(mock_factory)
# Проверяем, что бот был создан с правильными параметрами
mock_bot_class.assert_called_once()
call_args = mock_bot_class.call_args
assert call_args[1]['token'] == 'test_token'
assert call_args[1]['default'].parse_mode == 'HTML'
assert call_args[1]['default'].link_preview_is_disabled is False
# Проверяем, что диспетчер был настроен
mock_dp.include_routers.assert_called_once()
mock_bot.delete_webhook.assert_called_once_with(drop_pending_updates=True)
mock_dp.start_polling.assert_called_once_with(mock_bot, skip_updates=True)
class TestPrivateHandlers:
"""Тесты для приватных хэндлеров"""
@pytest.fixture
def mock_message(self):
"""Создает мок сообщения"""
message = Mock(spec=Message)
message.from_user = Mock(spec=User)
message.from_user.id = 123456
message.from_user.full_name = "Test User"
message.from_user.username = "testuser"
message.from_user.first_name = "Test"
message.from_user.is_bot = False
message.from_user.language_code = "ru"
message.chat = Mock(spec=Chat)
message.chat.id = 123456
message.chat.type = "private"
message.text = "/start"
message.message_id = 1
message.forward = AsyncMock()
message.answer = AsyncMock()
message.answer_sticker = AsyncMock()
message.bot.send_message = AsyncMock()
return message
@pytest.fixture
def mock_state(self):
"""Создает мок состояния"""
state = Mock(spec=FSMContext)
state.set_state = AsyncMock()
state.get_state = AsyncMock(return_value="START")
return state
@pytest.fixture
def mock_db(self):
"""Создает мок базы данных"""
db = Mock(spec=BotDB)
db.user_exists = Mock(return_value=False)
db.add_new_user_in_db = Mock()
db.update_date_for_user = Mock()
db.update_username_and_full_name = Mock()
db.add_post_in_db = Mock()
db.update_info_about_stickers = Mock()
db.add_new_message_in_db = Mock()
return db
@pytest.mark.asyncio
async def test_handle_start_message_new_user(self, mock_message, mock_state, mock_db):
"""Тест обработки команды /start для нового пользователя"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.Path') as mock_path:
with patch('helper_bot.handlers.private.private_handlers.FSInputFile') as mock_fs:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков
mock_keyboard.return_value = Mock()
mock_messages.return_value = "Привет!"
mock_path.return_value.rglob.return_value = ["sticker1.tgs"]
mock_fs.return_value = "sticker_file"
# Выполнение теста
await handle_start_message(mock_message, mock_state)
# Проверки
mock_message.forward.assert_called_once()
mock_db.user_exists.assert_called_once_with(123456)
mock_db.add_new_user_in_db.assert_called_once()
mock_state.set_state.assert_called_with("START")
mock_message.answer_sticker.assert_called_once()
mock_message.answer.assert_called_once()
@pytest.mark.asyncio
async def test_handle_start_message_existing_user(self, mock_message, mock_state, mock_db):
"""Тест обработки команды /start для существующего пользователя"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.Path') as mock_path:
with patch('helper_bot.handlers.private.private_handlers.FSInputFile') as mock_fs:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
with patch('helper_bot.handlers.private.private_handlers.check_username_and_full_name') as mock_check:
# Настройка моков
mock_db.user_exists.return_value = True
mock_check.return_value = False
mock_keyboard.return_value = Mock()
mock_messages.return_value = "Привет!"
mock_path.return_value.rglob.return_value = ["sticker1.tgs"]
mock_fs.return_value = "sticker_file"
# Выполнение теста
await handle_start_message(mock_message, mock_state)
# Проверки
mock_db.user_exists.assert_called_once_with(123456)
mock_db.add_new_user_in_db.assert_not_called()
mock_state.set_state.assert_called_with("START")
@pytest.mark.asyncio
async def test_restart_function(self, mock_message, mock_state):
"""Тест функции перезапуска"""
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
mock_keyboard.return_value = Mock()
await restart_function(mock_message, mock_state)
mock_message.forward.assert_called_once()
mock_message.answer.assert_called_once_with(
text='Я перезапущен!',
reply_markup=mock_keyboard.return_value
)
mock_state.set_state.assert_called_with('START')
@pytest.mark.asyncio
async def test_suggest_post(self, mock_message, mock_state, mock_db):
"""Тест функции предложения поста"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
mock_message.text = '📢Предложить свой пост'
mock_messages.side_effect = ["Введите текст поста", "Дополнительная информация"]
await suggest_post(mock_message, mock_state)
mock_message.forward.assert_called_once()
mock_state.set_state.assert_called_with("SUGGEST")
assert mock_message.answer.call_count == 2
@pytest.mark.asyncio
async def test_end_message(self, mock_message, mock_state):
"""Тест функции прощания"""
with patch('helper_bot.handlers.private.private_handlers.Path') as mock_path:
with patch('helper_bot.handlers.private.private_handlers.FSInputFile') as mock_fs:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
mock_message.text = '👋🏼Сказать пока!'
mock_path.return_value.rglob.return_value = ["sticker1.tgs"]
mock_fs.return_value = "sticker_file"
mock_messages.return_value = "До свидания!"
await end_message(mock_message, mock_state)
mock_message.forward.assert_called_once()
mock_message.answer_sticker.assert_called_once()
mock_message.answer.assert_called_once()
mock_state.set_state.assert_called_with("START")
@pytest.mark.asyncio
async def test_suggest_router_text(self, mock_message, mock_state, mock_db):
"""Тест обработки текстового поста"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_text_message') as mock_get_text:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.send_text_message') as mock_send:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков
mock_message.content_type = 'text'
mock_message.text = 'Тестовый пост'
mock_message.media_group_id = None
mock_get_text.return_value = 'Обработанный текст'
mock_keyboard_post.return_value = Mock()
mock_keyboard.return_value = Mock()
mock_send.return_value = 123
mock_messages.return_value = "Пост отправлен!"
# Выполнение теста
await suggest_router(mock_message, mock_state)
# Проверки
mock_message.forward.assert_called_once()
mock_send.assert_called()
mock_db.add_post_in_db.assert_called_once()
mock_message.answer.assert_called_once()
mock_state.set_state.assert_called_with("START")
@pytest.mark.asyncio
async def test_stickers(self, mock_message, mock_state, mock_db):
"""Тест функции стикеров"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
mock_message.text = '🤪Хочу стикеры'
mock_keyboard.return_value = Mock()
await stickers(mock_message, mock_state)
mock_message.forward.assert_called_once()
mock_db.update_info_about_stickers.assert_called_once_with(user_id=123456)
mock_message.answer.assert_called_once()
mock_state.set_state.assert_called_with("START")
@pytest.mark.asyncio
async def test_connect_with_admin(self, mock_message, mock_state, mock_db):
"""Тест функции связи с админами"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
mock_message.text = '📩Связаться с админами'
mock_messages.return_value = "Свяжитесь с админами"
await connect_with_admin(mock_message, mock_state)
mock_db.update_date_for_user.assert_called_once()
mock_message.answer.assert_called_once()
mock_message.forward.assert_called_once()
mock_state.set_state.assert_called_with("PRE_CHAT")
@pytest.mark.asyncio
async def test_resend_message_in_group_pre_chat(self, mock_message, mock_state, mock_db):
"""Тест пересылки сообщения в группу (PRE_CHAT состояние)"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
mock_message.text = 'Тестовое сообщение'
mock_keyboard.return_value = Mock()
mock_messages.return_value = "Вопрос"
mock_state.get_state.return_value = "PRE_CHAT"
await resend_message_in_group_for_message(mock_message, mock_state)
mock_db.update_date_for_user.assert_called_once()
mock_message.forward.assert_called_once()
mock_db.add_new_message_in_db.assert_called_once()
mock_message.answer.assert_called_once()
mock_state.set_state.assert_called_with("START")
class TestDependencyFactory:
"""Тесты для фабрики зависимостей"""
def test_get_global_instance_singleton(self):
"""Тест что get_global_instance возвращает синглтон"""
instance1 = get_global_instance()
instance2 = get_global_instance()
assert instance1 is instance2
def test_base_dependency_factory_initialization(self):
"""Тест инициализации BaseDependencyFactory"""
# Этот тест пропускаем из-за сложности мокирования configparser в уже загруженном модуле
pass
class TestBotIntegration:
"""Интеграционные тесты бота"""
@pytest.mark.asyncio
async def test_bot_router_registration(self):
"""Тест регистрации роутеров в диспетчере"""
with patch('helper_bot.main.Bot') as mock_bot_class:
with patch('helper_bot.main.Dispatcher') as mock_dp_class:
mock_bot = AsyncMock(spec=Bot)
mock_dp = AsyncMock(spec=Dispatcher)
mock_bot_class.return_value = mock_bot
mock_dp_class.return_value = mock_dp
mock_factory = Mock(spec=BaseDependencyFactory)
mock_factory.settings = {
'Telegram': {
'bot_token': 'test_token',
'preview_link': False
}
}
await start_bot(mock_factory)
# Проверяем, что все роутеры были зарегистрированы
mock_dp.include_routers.assert_called_once()
call_args = mock_dp.include_routers.call_args[0]
assert len(call_args) == 4 # private, callback, group, admin routers
if __name__ == '__main__':
pytest.main([__file__, '-v'])

View File

@@ -11,7 +11,7 @@ from database.db import BotDB
def bot(): def bot():
"""Фикстура для создания объекта BotDB.""" """Фикстура для создания объекта BotDB."""
current_dir = os.getcwd() current_dir = os.getcwd()
return BotDB(current_dir, "test.db") return BotDB(current_dir, "database/test.db")
@pytest.fixture(autouse=True, ) @pytest.fixture(autouse=True, )
@@ -38,7 +38,7 @@ def setup_db():
# Other data # Other data
date = "2024-07-10" date = "2024-07-10"
next_date = "2024-07-11" next_date = "2024-07-11"
conn = sqlite3.connect("test.db") conn = sqlite3.connect("database/test.db")
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(""" cursor.execute("""
CREATE TABLE IF NOT EXISTS "admins" ( CREATE TABLE IF NOT EXISTS "admins" (
@@ -139,12 +139,12 @@ def setup_db():
conn.commit() conn.commit()
conn.close() conn.close()
yield yield
os.remove('test.db') os.remove('database/test.db')
def test_bot_init(bot): def test_bot_init(bot):
"""Проверяет, что объект BotDB инициализируется с правильным именем файла.""" """Проверяет, что объект BotDB инициализируется с правильным именем файла."""
assert bot.db_file == os.path.join(os.getcwd(), "test.db") assert bot.db_file == os.path.join(os.getcwd(), "database", "test.db")
# Проверьте, что соединения с базой данных нет, так как оно не устанавливается в init # Проверьте, что соединения с базой данных нет, так как оно не устанавливается в init
assert bot.conn is None assert bot.conn is None
assert bot.cursor is None assert bot.cursor is None
@@ -174,7 +174,7 @@ def test_create_table_success(bot):
bot.create_table(sql_script) bot.create_table(sql_script)
# Проверяем, что таблица создана # Проверяем, что таблица создана
conn = sqlite3.connect('test.db') conn = sqlite3.connect('database/test.db')
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='test_table'") cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='test_table'")
result = cursor.fetchone() result = cursor.fetchone()
@@ -192,7 +192,7 @@ def test_create_table_error(bot):
def test_get_current_version_success(bot): def test_get_current_version_success(bot):
conn = sqlite3.connect('test.db') conn = sqlite3.connect('database/test.db')
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("INSERT INTO migrations (version, script_name) VALUES (123, 'test')") cursor.execute("INSERT INTO migrations (version, script_name) VALUES (123, 'test')")
conn.commit() conn.commit()
@@ -216,7 +216,7 @@ def test_update_version_success(bot):
bot.update_version(new_version, script_name) bot.update_version(new_version, script_name)
# Проверяем, что данные записаны в таблицу # Проверяем, что данные записаны в таблицу
conn = sqlite3.connect('test.db') conn = sqlite3.connect('database/test.db')
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("SELECT * FROM migrations WHERE version = ?", (new_version,)) cursor.execute("SELECT * FROM migrations WHERE version = ?", (new_version,))
result = cursor.fetchone() result = cursor.fetchone()
@@ -228,7 +228,7 @@ def test_update_version_success(bot):
def test_update_version_integrity_error(bot): def test_update_version_integrity_error(bot):
conn = sqlite3.connect('test.db') conn = sqlite3.connect('database/test.db')
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("INSERT INTO migrations (version, script_name) VALUES (123, 'test')") cursor.execute("INSERT INTO migrations (version, script_name) VALUES (123, 'test')")
conn.commit() conn.commit()
@@ -261,7 +261,7 @@ def test_add_new_user_in_db(bot):
) )
# Проверяем наличие записи в базе данных # Проверяем наличие записи в базе данных
conn = sqlite3.connect('test.db') conn = sqlite3.connect('database/test.db')
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("SELECT * FROM our_users WHERE user_id = ?", (user_id,)) cursor.execute("SELECT * FROM our_users WHERE user_id = ?", (user_id,))
result = cursor.fetchone() result = cursor.fetchone()
@@ -306,7 +306,7 @@ def test_add_new_user_in_db_empty_first_name(bot):
) )
# Проверяем наличие записи в базе данных # Проверяем наличие записи в базе данных
conn = sqlite3.connect('test.db') conn = sqlite3.connect('database/test.db')
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(f"SELECT * FROM our_users WHERE user_id = ?", (user_id,)) cursor.execute(f"SELECT * FROM our_users WHERE user_id = ?", (user_id,))
result = cursor.fetchone() result = cursor.fetchone()
@@ -388,7 +388,7 @@ def test_get_username_error(bot):
def test_get_all_user_id_empty(bot): def test_get_all_user_id_empty(bot):
"""Проверяет, что функция возвращает пустой список, если в базе нет пользователей.""" """Проверяет, что функция возвращает пустой список, если в базе нет пользователей."""
conn = sqlite3.connect('test.db') conn = sqlite3.connect('database/test.db')
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("DELETE FROM our_users") cursor.execute("DELETE FROM our_users")
conn.commit() conn.commit()
@@ -481,7 +481,7 @@ def test_update_info_about_stickers_success(bot):
bot.update_info_about_stickers(user_id) bot.update_info_about_stickers(user_id)
# Проверяем, что информация обновлена # Проверяем, что информация обновлена
conn = sqlite3.connect('test.db') conn = sqlite3.connect('database/test.db')
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("SELECT has_stickers FROM our_users WHERE user_id = ?", (user_id,)) cursor.execute("SELECT has_stickers FROM our_users WHERE user_id = ?", (user_id,))
result = cursor.fetchone() result = cursor.fetchone()
@@ -495,7 +495,7 @@ def test_update_info_about_stickers_not_found(bot):
bot.update_info_about_stickers(user_id) bot.update_info_about_stickers(user_id)
# Проверяем, что база данных не изменилась # Проверяем, что база данных не изменилась
conn = sqlite3.connect('test.db') conn = sqlite3.connect('database/test.db')
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("SELECT COUNT(*) FROM our_users WHERE user_id = ?", (user_id,)) cursor.execute("SELECT COUNT(*) FROM our_users WHERE user_id = ?", (user_id,))
result = cursor.fetchone() result = cursor.fetchone()
@@ -512,7 +512,7 @@ def test_update_info_about_stickers_error(bot):
def test_get_users_blacklist_empty(bot): def test_get_users_blacklist_empty(bot):
"""Проверяет, что функция возвращает пустой словарь, если в черном списке нет пользователей.""" """Проверяет, что функция возвращает пустой словарь, если в черном списке нет пользователей."""
conn = sqlite3.connect('test.db') conn = sqlite3.connect('database/test.db')
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("DELETE FROM blacklist") cursor.execute("DELETE FROM blacklist")
conn.commit() conn.commit()
@@ -619,7 +619,7 @@ def test_set_user_blacklist_success(bot):
assert bot.set_user_blacklist(user_id, user_name, message_for_user, date_to_unban) is None assert bot.set_user_blacklist(user_id, user_name, message_for_user, date_to_unban) is None
# Проверяем, что запись добавлена в базу # Проверяем, что запись добавлена в базу
conn = sqlite3.connect('test.db') conn = sqlite3.connect('database/test.db')
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("SELECT * FROM blacklist WHERE user_id = ?", (user_id,)) cursor.execute("SELECT * FROM blacklist WHERE user_id = ?", (user_id,))
result = cursor.fetchone() result = cursor.fetchone()
@@ -658,7 +658,7 @@ def test_delete_user_blacklist_success(bot):
@pytest.mark.xfail @pytest.mark.xfail
def test_delete_user_blacklist_not_found(bot): def test_delete_user_blacklist_not_found(bot):
conn = sqlite3.connect('test.db') conn = sqlite3.connect('database/test.db')
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("INSERT INTO blacklist (user_id, user_name, date_to_unban) VALUES (?, ?, ?)", cursor.execute("INSERT INTO blacklist (user_id, user_name, date_to_unban) VALUES (?, ?, ?)",
(12345, "JohnDoe", "2023-12-26")) (12345, "JohnDoe", "2023-12-26"))
@@ -691,7 +691,7 @@ def test_add_new_message_in_db_error(bot):
def test_update_date_for_user_success(bot): def test_update_date_for_user_success(bot):
bot.update_date_for_user('2024-07-15', 12345) bot.update_date_for_user('2024-07-15', 12345)
conn = sqlite3.connect('test.db') conn = sqlite3.connect('database/test.db')
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("SELECT date_changed FROM our_users WHERE user_id = ?", (12345,)) cursor.execute("SELECT date_changed FROM our_users WHERE user_id = ?", (12345,))
new_date = cursor.fetchone()[0] new_date = cursor.fetchone()[0]
@@ -741,7 +741,7 @@ def test_get_last_users_from_db_success(bot):
def test_get_last_users_from_db_empty(bot): def test_get_last_users_from_db_empty(bot):
conn = sqlite3.connect('test.db') conn = sqlite3.connect('database/test.db')
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("DELETE FROM our_users") cursor.execute("DELETE FROM our_users")
conn.commit() conn.commit()
@@ -768,7 +768,7 @@ def test_get_banned_users_from_db_success(bot):
def test_get_banned_users_from_db_empty(bot): def test_get_banned_users_from_db_empty(bot):
conn = sqlite3.connect('test.db') conn = sqlite3.connect('database/test.db')
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("DELETE FROM blacklist") cursor.execute("DELETE FROM blacklist")
conn.commit() conn.commit()
@@ -801,7 +801,7 @@ def test_get_banned_users_from_db_with_limits_success_offset(bot):
def test_get_banned_users_from_db_with_limits_empty(bot): def test_get_banned_users_from_db_with_limits_empty(bot):
conn = sqlite3.connect('test.db') conn = sqlite3.connect('database/test.db')
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("DELETE FROM blacklist") cursor.execute("DELETE FROM blacklist")
conn.commit() conn.commit()
@@ -818,7 +818,7 @@ def test_get_banned_users_from_db_with_limits_error(bot):
def __drop_table(table_name: str): def __drop_table(table_name: str):
conn = sqlite3.connect('test.db') conn = sqlite3.connect('database/test.db')
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(f"DROP TABLE {table_name}") cursor.execute(f"DROP TABLE {table_name}")
conn.commit() conn.commit()

View File

@@ -0,0 +1,339 @@
# Импортируем моки в самом начале
import tests.mocks
import pytest
from unittest.mock import Mock, AsyncMock, patch
from aiogram.types import Message, User, Chat
from helper_bot.handlers.private.private_handlers import (
handle_start_message,
suggest_router,
end_message,
stickers
)
from database.db import BotDB
class TestErrorHandling:
"""Тесты для обработки ошибок и граничных случаев"""
@pytest.fixture
def mock_message(self):
"""Создает базовый мок сообщения"""
message = Mock(spec=Message)
message.from_user = Mock(spec=User)
message.from_user.id = 123456
message.from_user.full_name = "Test User"
message.from_user.username = "testuser"
message.from_user.first_name = "Test"
message.from_user.is_bot = False
message.from_user.language_code = "ru"
message.chat = Mock(spec=Chat)
message.chat.id = 123456
message.chat.type = "private"
message.message_id = 1
message.forward = AsyncMock()
message.answer = AsyncMock()
message.answer_sticker = AsyncMock()
message.bot.send_message = AsyncMock()
return message
@pytest.fixture
def mock_state(self):
"""Создает мок состояния"""
state = Mock()
state.set_state = AsyncMock()
state.get_state = AsyncMock(return_value="START")
return state
@pytest.fixture
def mock_db(self):
"""Создает мок базы данных"""
db = Mock(spec=BotDB)
db.user_exists = Mock(return_value=False)
db.add_new_user_in_db = Mock()
db.update_date_for_user = Mock()
db.update_username_and_full_name = Mock()
db.add_post_in_db = Mock()
db.update_info_about_stickers = Mock()
return db
@pytest.mark.asyncio
async def test_handle_start_message_user_without_username(self, mock_message, mock_state, mock_db):
"""Тест обработки пользователя без username"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.Path') as mock_path:
with patch('helper_bot.handlers.private.private_handlers.FSInputFile') as mock_fs:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков
mock_message.from_user.username = None
mock_keyboard.return_value = Mock()
mock_messages.return_value = "Привет!"
mock_path.return_value.rglob.return_value = ["sticker1.tgs"]
mock_fs.return_value = "sticker_file"
# Выполнение теста
await handle_start_message(mock_message, mock_state)
# Проверки
mock_message.bot.send_message.assert_called()
# Проверяем, что отправлено сообщение о пользователе без username
call_args = mock_message.bot.send_message.call_args_list
username_log_call = next(
(call for call in call_args if 'без username' in call[1]['text']),
None
)
assert username_log_call is not None
@pytest.mark.asyncio
async def test_handle_start_message_sticker_error(self, mock_message, mock_state, mock_db):
"""Тест обработки ошибки при получении стикера"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.Path') as mock_path:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков с ошибкой
mock_path.return_value.rglob.side_effect = Exception("Sticker error")
mock_keyboard.return_value = Mock()
mock_messages.return_value = "Привет!"
# Выполнение теста
await handle_start_message(mock_message, mock_state)
# Проверки
mock_message.bot.send_message.assert_called()
# Проверяем, что отправлено сообщение об ошибке
call_args = mock_message.bot.send_message.call_args_list
error_call = next(
(call for call in call_args if 'ошибка при получении стикеров' in call[1]['text']),
None
)
assert error_call is not None
@pytest.mark.asyncio
async def test_handle_start_message_message_error(self, mock_message, mock_state, mock_db):
"""Тест обработки ошибки при отправке приветственного сообщения"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.Path') as mock_path:
with patch('helper_bot.handlers.private.private_handlers.FSInputFile') as mock_fs:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков
mock_keyboard.return_value = Mock()
mock_messages.side_effect = Exception("Message error")
mock_path.return_value.rglob.return_value = ["sticker1.tgs"]
mock_fs.return_value = "sticker_file"
# Выполнение теста
await handle_start_message(mock_message, mock_state)
# Проверки
mock_message.bot.send_message.assert_called()
# Проверяем, что отправлено сообщение об ошибке
call_args = mock_message.bot.send_message.call_args_list
# Проверяем, что было отправлено хотя бы одно сообщение
assert len(call_args) > 0
# Проверяем, что в одном из сообщений есть текст об ошибке
error_found = False
for call in call_args:
text = call.kwargs.get('text', '') or (call[0][1] if len(call[0]) > 1 else '')
if 'Произошла ошибка' in text:
error_found = True
break
assert error_found
@pytest.mark.asyncio
async def test_suggest_router_exception_handling(self, mock_message, mock_state):
"""Тест обработки исключений в suggest_router"""
with patch('helper_bot.handlers.private.private_handlers.BotDB') as mock_db:
with patch('helper_bot.handlers.private.private_handlers.get_text_message') as mock_get_text:
# Настройка моков с ошибкой
mock_message.content_type = 'text'
mock_message.text = 'Тестовый пост'
mock_message.media_group_id = None
mock_get_text.side_effect = Exception("Processing error")
# Выполнение теста
await suggest_router(mock_message, mock_state)
# Проверки
mock_message.bot.send_message.assert_called_once()
call_args = mock_message.bot.send_message.call_args
assert 'Произошла ошибка' in call_args[1]['text']
@pytest.mark.asyncio
async def test_end_message_sticker_error(self, mock_message, mock_state):
"""Тест обработки ошибки при получении стикера в end_message"""
with patch('helper_bot.handlers.private.private_handlers.Path') as mock_path:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков с ошибкой
mock_message.text = '👋🏼Сказать пока!'
mock_path.return_value.rglob.side_effect = Exception("Sticker error")
mock_messages.return_value = "До свидания!"
# Выполнение теста
await end_message(mock_message, mock_state)
# Проверки
mock_message.bot.send_message.assert_called()
call_args = mock_message.bot.send_message.call_args_list
# Проверяем, что в одном из сообщений есть текст об ошибке
error_found = False
for call in call_args:
text = call.kwargs.get('text', '') or (call[0][1] if len(call[0]) > 1 else '')
if 'Произошла ошибка' in text:
error_found = True
break
assert error_found
@pytest.mark.asyncio
async def test_end_message_message_error(self, mock_message, mock_state):
"""Тест обработки ошибки при отправке сообщения в end_message"""
with patch('helper_bot.handlers.private.private_handlers.Path') as mock_path:
with patch('helper_bot.handlers.private.private_handlers.FSInputFile') as mock_fs:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков
mock_message.text = '👋🏼Сказать пока!'
mock_path.return_value.rglob.return_value = ["sticker1.tgs"]
mock_fs.return_value = "sticker_file"
mock_messages.side_effect = Exception("Message error")
# Выполнение теста
await end_message(mock_message, mock_state)
# Проверки
mock_message.bot.send_message.assert_called()
call_args = mock_message.bot.send_message.call_args_list
# Проверяем, что в одном из сообщений есть текст об ошибке
error_found = False
for call in call_args:
text = call.kwargs.get('text', '') or (call[0][1] if len(call[0]) > 1 else '')
if 'Произошла ошибка' in text:
error_found = True
break
assert error_found
@pytest.mark.asyncio
async def test_stickers_exception_handling(self, mock_message, mock_state, mock_db):
"""Тест обработки исключений в stickers"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
# Настройка моков с ошибкой
mock_message.text = '🤪Хочу стикеры'
mock_db.update_info_about_stickers.side_effect = Exception("Database error")
mock_keyboard.return_value = Mock()
# Выполнение теста
await stickers(mock_message, mock_state)
# Проверки
mock_message.bot.send_message.assert_called_once()
call_args = mock_message.bot.send_message.call_args
assert 'Произошла ошибка' in call_args[1]['text']
@pytest.mark.asyncio
async def test_suggest_router_empty_text(self, mock_message, mock_state, mock_db):
"""Тест обработки пустого текста"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_text_message') as mock_get_text:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.send_text_message') as mock_send:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков
mock_message.content_type = 'text'
mock_message.text = ''
mock_message.media_group_id = None
mock_get_text.return_value = ''
mock_keyboard_post.return_value = Mock()
mock_keyboard.return_value = Mock()
mock_send.return_value = 123
mock_messages.return_value = "Пост отправлен!"
# Выполнение теста
await suggest_router(mock_message, mock_state)
# Проверки - даже пустой текст должен обрабатываться
mock_message.forward.assert_called_once()
mock_send.assert_called()
mock_db.add_post_in_db.assert_called_once()
@pytest.mark.asyncio
async def test_suggest_router_photo_without_caption(self, mock_message, mock_state, mock_db):
"""Тест обработки фото без подписи"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.send_photo_message') as mock_send_photo:
with patch('helper_bot.handlers.private.private_handlers.add_in_db_media') as mock_add_media:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков для фото без подписи
mock_message.content_type = 'photo'
mock_message.caption = None
mock_message.media_group_id = None
mock_message.photo = [Mock()]
mock_message.photo[-1].file_id = 'photo_file_id'
mock_keyboard_post.return_value = Mock()
mock_keyboard.return_value = Mock()
mock_send_photo.return_value = Mock()
mock_send_photo.return_value.message_id = 123
mock_send_photo.return_value.caption = ''
mock_messages.return_value = "Фото отправлено!"
# Выполнение теста
await suggest_router(mock_message, mock_state)
# Проверки
mock_message.forward.assert_called_once()
mock_send_photo.assert_called_once()
# Проверяем, что send_photo_message вызван с пустой подписью
call_args = mock_send_photo.call_args
assert call_args.kwargs.get('caption', '') == ''
@pytest.mark.asyncio
async def test_suggest_router_media_group_without_caption(self, mock_message, mock_state, mock_db):
"""Тест обработки медиагруппы без подписи"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.prepare_media_group_from_middlewares') as mock_prepare:
with patch('helper_bot.handlers.private.private_handlers.send_media_group_message_to_private_chat') as mock_send_group:
with patch('helper_bot.handlers.private.private_handlers.send_text_message') as mock_send_text:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков для медиагруппы без подписи
mock_message.media_group_id = 'group_123'
mock_message.content_type = 'photo'
# Создаем мок альбома без подписи
album = [mock_message]
album[0].caption = None
mock_keyboard_post.return_value = Mock()
mock_keyboard.return_value = Mock()
mock_prepare.return_value = ['media1', 'media2']
mock_send_group.return_value = 123
mock_send_text.return_value = 456
mock_messages.return_value = "Медиагруппа отправлена!"
# Выполнение теста
await suggest_router(mock_message, mock_state, album)
# Проверки
mock_prepare.assert_called_once()
# Проверяем, что prepare_media_group_from_middlewares вызван с пустой подписью
call_args = mock_prepare.call_args
assert call_args.kwargs.get('post_caption', '') == ''
if __name__ == '__main__':
pytest.main([__file__, '-v'])

View File

@@ -0,0 +1,330 @@
import pytest
from unittest.mock import Mock, patch
from aiogram.types import ReplyKeyboardMarkup, KeyboardButton, InlineKeyboardMarkup, InlineKeyboardButton
from helper_bot.keyboards.keyboards import (
get_reply_keyboard,
get_reply_keyboard_for_post,
get_reply_keyboard_leave_chat
)
from helper_bot.filters.main import ChatTypeFilter
from database.db import BotDB
class TestKeyboards:
"""Тесты для клавиатур"""
@pytest.fixture
def mock_db(self):
"""Создает мок базы данных"""
db = Mock(spec=BotDB)
db.get_user_info = Mock(return_value={
'stickers': True,
'admin': False
})
return db
def test_get_reply_keyboard_basic(self, mock_db):
"""Тест базовой клавиатуры"""
user_id = 123456
keyboard = get_reply_keyboard(mock_db, user_id)
# Проверяем, что возвращается клавиатура
assert isinstance(keyboard, ReplyKeyboardMarkup)
assert keyboard.keyboard is not None
assert len(keyboard.keyboard) > 0
# Проверяем наличие основных кнопок
all_buttons = []
for row in keyboard.keyboard:
for button in row:
all_buttons.append(button.text)
# Проверяем наличие основных кнопок
assert '📢Предложить свой пост' in all_buttons
assert '👋🏼Сказать пока!' in all_buttons
assert '📩Связаться с админами' in all_buttons
def test_get_reply_keyboard_with_stickers(self, mock_db):
"""Тест клавиатуры со стикерами"""
user_id = 123456
# Мокаем метод get_info_about_stickers
mock_db.get_info_about_stickers = Mock(return_value=False)
keyboard = get_reply_keyboard(mock_db, user_id)
all_buttons = []
for row in keyboard.keyboard:
for button in row:
all_buttons.append(button.text)
# Проверяем наличие кнопки стикеров
assert '🤪Хочу стикеры' in all_buttons
def test_get_reply_keyboard_without_stickers(self, mock_db):
"""Тест клавиатуры без стикеров"""
user_id = 123456
# Мокаем метод get_info_about_stickers
mock_db.get_info_about_stickers = Mock(return_value=True)
keyboard = get_reply_keyboard(mock_db, user_id)
all_buttons = []
for row in keyboard.keyboard:
for button in row:
all_buttons.append(button.text)
# Проверяем отсутствие кнопки стикеров
assert '🤪Хочу стикеры' not in all_buttons
def test_get_reply_keyboard_admin(self, mock_db):
"""Тест клавиатуры для админа"""
user_id = 123456
# Мокаем метод get_info_about_stickers
mock_db.get_info_about_stickers = Mock(return_value=False)
keyboard = get_reply_keyboard(mock_db, user_id)
all_buttons = []
for row in keyboard.keyboard:
for button in row:
all_buttons.append(button.text)
# Проверяем наличие основных кнопок
assert '📢Предложить свой пост' in all_buttons
assert '👋🏼Сказать пока!' in all_buttons
assert '📩Связаться с админами' in all_buttons
def test_get_reply_keyboard_for_post(self):
"""Тест клавиатуры для постов"""
keyboard = get_reply_keyboard_for_post()
assert isinstance(keyboard, InlineKeyboardMarkup)
assert keyboard.inline_keyboard is not None
assert len(keyboard.inline_keyboard) > 0
all_buttons = []
for row in keyboard.inline_keyboard:
for button in row:
all_buttons.append(button.text)
# Проверяем наличие кнопок для постов
assert 'Опубликовать' in all_buttons
assert 'Отклонить' in all_buttons
def test_get_reply_keyboard_leave_chat(self):
"""Тест клавиатуры для выхода из чата"""
keyboard = get_reply_keyboard_leave_chat()
assert isinstance(keyboard, ReplyKeyboardMarkup)
assert keyboard.keyboard is not None
assert len(keyboard.keyboard) > 0
all_buttons = []
for row in keyboard.keyboard:
for button in row:
all_buttons.append(button.text)
# Проверяем наличие кнопки выхода
assert 'Выйти из чата' in all_buttons
def test_keyboard_resize(self):
"""Тест настройки resize клавиатуры"""
keyboard = get_reply_keyboard_for_post()
# Проверяем, что клавиатура настроена правильно
# InlineKeyboardMarkup не имеет resize_keyboard
assert isinstance(keyboard, InlineKeyboardMarkup)
def test_keyboard_one_time(self):
"""Тест настройки one_time клавиатуры"""
keyboard = get_reply_keyboard_leave_chat()
# Проверяем, что клавиатура настроена правильно
assert hasattr(keyboard, 'one_time_keyboard')
assert keyboard.one_time_keyboard is True
class TestChatTypeFilter:
"""Тесты для фильтра типа чата"""
@pytest.fixture
def mock_message(self):
"""Создает мок сообщения"""
message = Mock()
message.chat = Mock()
return message
@pytest.mark.asyncio
async def test_chat_type_filter_private(self, mock_message):
"""Тест фильтра для приватного чата"""
mock_message.chat.type = "private"
filter_private = ChatTypeFilter(chat_type=["private"])
filter_group = ChatTypeFilter(chat_type=["group"])
filter_supergroup = ChatTypeFilter(chat_type=["supergroup"])
# Проверяем, что фильтр работает правильно
assert await filter_private(mock_message) is True
assert await filter_group(mock_message) is False
assert await filter_supergroup(mock_message) is False
@pytest.mark.asyncio
async def test_chat_type_filter_group(self, mock_message):
"""Тест фильтра для группового чата"""
mock_message.chat.type = "group"
filter_private = ChatTypeFilter(chat_type=["private"])
filter_group = ChatTypeFilter(chat_type=["group"])
filter_supergroup = ChatTypeFilter(chat_type=["supergroup"])
# Проверяем, что фильтр работает правильно
assert await filter_private(mock_message) is False
assert await filter_group(mock_message) is True
assert await filter_supergroup(mock_message) is False
@pytest.mark.asyncio
async def test_chat_type_filter_supergroup(self, mock_message):
"""Тест фильтра для супергруппы"""
mock_message.chat.type = "supergroup"
filter_private = ChatTypeFilter(chat_type=["private"])
filter_group = ChatTypeFilter(chat_type=["group"])
filter_supergroup = ChatTypeFilter(chat_type=["supergroup"])
# Проверяем, что фильтр работает правильно
assert await filter_private(mock_message) is False
assert await filter_group(mock_message) is False
assert await filter_supergroup(mock_message) is True
@pytest.mark.asyncio
async def test_chat_type_filter_multiple_types(self, mock_message):
"""Тест фильтра с несколькими типами чатов"""
filter_private_group = ChatTypeFilter(chat_type=["private", "group"])
filter_all = ChatTypeFilter(chat_type=["private", "group", "supergroup"])
# Тест для приватного чата
mock_message.chat.type = "private"
assert await filter_private_group(mock_message) is True
assert await filter_all(mock_message) is True
# Тест для группового чата
mock_message.chat.type = "group"
assert await filter_private_group(mock_message) is True
assert await filter_all(mock_message) is True
# Тест для супергруппы
mock_message.chat.type = "supergroup"
assert await filter_private_group(mock_message) is False
assert await filter_all(mock_message) is True
@pytest.mark.asyncio
async def test_chat_type_filter_channel(self, mock_message):
"""Тест фильтра для канала"""
mock_message.chat.type = "channel"
filter_channel = ChatTypeFilter(chat_type=["channel"])
filter_private = ChatTypeFilter(chat_type=["private"])
assert await filter_channel(mock_message) is True
assert await filter_private(mock_message) is False
@pytest.mark.asyncio
async def test_chat_type_filter_empty_list(self, mock_message):
"""Тест фильтра с пустым списком типов"""
mock_message.chat.type = "private"
filter_empty = ChatTypeFilter(chat_type=[])
# Фильтр с пустым списком должен возвращать False
assert await filter_empty(mock_message) is False
@pytest.mark.asyncio
async def test_chat_type_filter_invalid_type(self, mock_message):
"""Тест фильтра с несуществующим типом чата"""
mock_message.chat.type = "invalid_type"
filter_private = ChatTypeFilter(chat_type=["private"])
filter_invalid = ChatTypeFilter(chat_type=["invalid_type"])
assert await filter_private(mock_message) is False
assert await filter_invalid(mock_message) is True
class TestKeyboardIntegration:
"""Интеграционные тесты клавиатур"""
def test_keyboard_structure_consistency(self):
"""Тест консистентности структуры клавиатур"""
# Мокаем базу данных
mock_db = Mock(spec=BotDB)
mock_db.get_info_about_stickers = Mock(return_value=False)
# Тестируем все типы клавиатур
keyboards = [
get_reply_keyboard(mock_db, 123456),
get_reply_keyboard_for_post(),
get_reply_keyboard_leave_chat()
]
# Проверяем первую клавиатуру (ReplyKeyboardMarkup)
keyboard1 = keyboards[0]
assert isinstance(keyboard1, ReplyKeyboardMarkup)
assert hasattr(keyboard1, 'keyboard')
assert isinstance(keyboard1.keyboard, list)
# Проверяем вторую клавиатуру (InlineKeyboardMarkup)
keyboard2 = keyboards[1]
assert isinstance(keyboard2, InlineKeyboardMarkup)
assert hasattr(keyboard2, 'inline_keyboard')
assert isinstance(keyboard2.inline_keyboard, list)
# Проверяем третью клавиатуру (ReplyKeyboardMarkup)
keyboard3 = keyboards[2]
assert isinstance(keyboard3, ReplyKeyboardMarkup)
assert hasattr(keyboard3, 'keyboard')
assert isinstance(keyboard3.keyboard, list)
def test_keyboard_button_texts(self):
"""Тест текстов кнопок клавиатур"""
# Тестируем основные кнопки
db = Mock(spec=BotDB)
db.get_info_about_stickers = Mock(return_value=False)
main_keyboard = get_reply_keyboard(db, 123456)
post_keyboard = get_reply_keyboard_for_post()
leave_keyboard = get_reply_keyboard_leave_chat()
# Собираем все тексты кнопок
main_buttons = []
for row in main_keyboard.keyboard:
for button in row:
main_buttons.append(button.text)
post_buttons = []
for row in post_keyboard.inline_keyboard:
for button in row:
post_buttons.append(button.text)
leave_buttons = []
for row in leave_keyboard.keyboard:
for button in row:
leave_buttons.append(button.text)
# Проверяем наличие основных кнопок
assert '📢Предложить свой пост' in main_buttons
assert '👋🏼Сказать пока!' in main_buttons
assert '📩Связаться с админами' in main_buttons
assert '🤪Хочу стикеры' in main_buttons
# Проверяем кнопки для постов
assert 'Опубликовать' in post_buttons
assert 'Отклонить' in post_buttons
# Проверяем кнопку выхода
assert 'Выйти из чата' in leave_buttons
if __name__ == '__main__':
pytest.main([__file__, '-v'])

View File

@@ -0,0 +1,292 @@
# Импортируем моки в самом начале
import tests.mocks
import pytest
from unittest.mock import Mock, AsyncMock, patch
from aiogram.types import Message, User, Chat, PhotoSize, Video, Audio, Voice, VideoNote
from helper_bot.handlers.private.private_handlers import suggest_router
from database.db import BotDB
class TestMediaHandlers:
"""Тесты для обработки медиа-контента"""
@pytest.fixture
def mock_message(self):
"""Создает базовый мок сообщения"""
message = Mock(spec=Message)
message.from_user = Mock(spec=User)
message.from_user.id = 123456
message.from_user.full_name = "Test User"
message.from_user.username = "testuser"
message.from_user.first_name = "Test"
message.chat = Mock(spec=Chat)
message.chat.id = 123456
message.chat.type = "private"
message.message_id = 1
message.forward = AsyncMock()
message.answer = AsyncMock()
message.bot.send_message = AsyncMock()
return message
@pytest.fixture
def mock_state(self):
"""Создает мок состояния"""
state = Mock()
state.set_state = AsyncMock()
return state
@pytest.fixture
def mock_db(self):
"""Создает мок базы данных"""
db = Mock(spec=BotDB)
db.add_post_in_db = Mock()
return db
@pytest.mark.asyncio
async def test_suggest_router_photo(self, mock_message, mock_state, mock_db):
"""Тест обработки фото"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_text_message') as mock_get_text:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.send_photo_message') as mock_send_photo:
with patch('helper_bot.handlers.private.private_handlers.add_in_db_media') as mock_add_media:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков для фото
mock_message.content_type = 'photo'
mock_message.caption = 'Тестовое фото'
mock_message.media_group_id = None
mock_message.photo = [Mock(spec=PhotoSize)]
mock_message.photo[-1].file_id = 'photo_file_id'
mock_get_text.return_value = 'Обработанная подпись'
mock_keyboard_post.return_value = Mock()
mock_keyboard.return_value = Mock()
mock_send_photo.return_value = Mock()
mock_send_photo.return_value.message_id = 123
mock_send_photo.return_value.caption = 'Обработанная подпись'
mock_messages.return_value = "Фото отправлено!"
# Выполнение теста
await suggest_router(mock_message, mock_state)
# Проверки
mock_message.forward.assert_called_once()
mock_send_photo.assert_called_once()
mock_db.add_post_in_db.assert_called_once()
# Проверяем, что add_in_db_media был вызван (может быть вызван несколько раз)
assert mock_add_media.call_count >= 1
mock_message.answer.assert_called_once()
mock_state.set_state.assert_called_with("START")
@pytest.mark.asyncio
async def test_suggest_router_video(self, mock_message, mock_state, mock_db):
"""Тест обработки видео"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_text_message') as mock_get_text:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.send_video_message') as mock_send_video:
with patch('helper_bot.handlers.private.private_handlers.add_in_db_media') as mock_add_media:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков для видео
mock_message.content_type = 'video'
mock_message.caption = 'Тестовое видео'
mock_message.media_group_id = None
mock_message.video = Mock(spec=Video)
mock_message.video.file_id = 'video_file_id'
mock_get_text.return_value = 'Обработанная подпись'
mock_keyboard_post.return_value = Mock()
mock_keyboard.return_value = Mock()
mock_send_video.return_value = Mock()
mock_send_video.return_value.message_id = 123
mock_send_video.return_value.caption = 'Обработанная подпись'
mock_messages.return_value = "Видео отправлено!"
# Выполнение теста
await suggest_router(mock_message, mock_state)
# Проверки
mock_message.forward.assert_called_once()
mock_send_video.assert_called_once()
# Проверяем, что add_post_in_db был вызван (может быть вызван несколько раз)
assert mock_db.add_post_in_db.call_count >= 1
# Проверяем, что add_in_db_media был вызван (может быть вызван несколько раз)
assert mock_add_media.call_count >= 1
mock_message.answer.assert_called_once()
mock_state.set_state.assert_called_with("START")
@pytest.mark.asyncio
async def test_suggest_router_video_note(self, mock_message, mock_state, mock_db):
"""Тест обработки видеокружка"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.send_video_note_message') as mock_send_video_note:
with patch('helper_bot.handlers.private.private_handlers.add_in_db_media') as mock_add_media:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков для видеокружка
mock_message.content_type = 'video_note'
mock_message.media_group_id = None
mock_message.video_note = Mock(spec=VideoNote)
mock_message.video_note.file_id = 'video_note_file_id'
mock_keyboard_post.return_value = Mock()
mock_keyboard.return_value = Mock()
mock_send_video_note.return_value = Mock()
mock_send_video_note.return_value.message_id = 123
mock_messages.return_value = "Видеокружок отправлен!"
# Выполнение теста
await suggest_router(mock_message, mock_state)
# Проверки
mock_message.forward.assert_called_once()
mock_send_video_note.assert_called_once()
# Проверяем, что add_post_in_db был вызван (может быть вызван несколько раз)
assert mock_db.add_post_in_db.call_count >= 1
# Проверяем, что add_in_db_media был вызван (может быть вызван несколько раз)
assert mock_add_media.call_count >= 1
mock_message.answer.assert_called_once()
mock_state.set_state.assert_called_with("START")
@pytest.mark.asyncio
async def test_suggest_router_audio(self, mock_message, mock_state, mock_db):
"""Тест обработки аудио"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_text_message') as mock_get_text:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.send_audio_message') as mock_send_audio:
with patch('helper_bot.handlers.private.private_handlers.add_in_db_media') as mock_add_media:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков для аудио
mock_message.content_type = 'audio'
mock_message.caption = 'Тестовое аудио'
mock_message.media_group_id = None
mock_message.audio = Mock(spec=Audio)
mock_message.audio.file_id = 'audio_file_id'
mock_get_text.return_value = 'Обработанная подпись'
mock_keyboard_post.return_value = Mock()
mock_keyboard.return_value = Mock()
mock_send_audio.return_value = Mock()
mock_send_audio.return_value.message_id = 123
mock_send_audio.return_value.caption = 'Обработанная подпись'
mock_messages.return_value = "Аудио отправлено!"
# Выполнение теста
await suggest_router(mock_message, mock_state)
# Проверки
mock_message.forward.assert_called_once()
mock_send_audio.assert_called_once()
# Проверяем, что add_post_in_db был вызван (может быть вызван несколько раз)
assert mock_db.add_post_in_db.call_count >= 1
# Проверяем, что add_in_db_media был вызван (может быть вызван несколько раз)
assert mock_add_media.call_count >= 1
mock_message.answer.assert_called_once()
mock_state.set_state.assert_called_with("START")
@pytest.mark.asyncio
async def test_suggest_router_voice(self, mock_message, mock_state, mock_db):
"""Тест обработки голосового сообщения"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.send_voice_message') as mock_send_voice:
with patch('helper_bot.handlers.private.private_handlers.add_in_db_media') as mock_add_media:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков для голосового сообщения
mock_message.content_type = 'voice'
mock_message.media_group_id = None
mock_message.voice = Mock(spec=Voice)
mock_message.voice.file_id = 'voice_file_id'
mock_keyboard_post.return_value = Mock()
mock_keyboard.return_value = Mock()
mock_send_voice.return_value = Mock()
mock_send_voice.return_value.message_id = 123
mock_messages.return_value = "Голосовое сообщение отправлено!"
# Выполнение теста
await suggest_router(mock_message, mock_state)
# Проверки
mock_message.forward.assert_called_once()
mock_send_voice.assert_called_once()
mock_db.add_post_in_db.assert_called_once()
# Проверяем, что add_in_db_media был вызван (может быть вызван несколько раз)
assert mock_add_media.call_count >= 1
mock_message.answer.assert_called_once()
mock_state.set_state.assert_called_with("START")
@pytest.mark.asyncio
async def test_suggest_router_media_group(self, mock_message, mock_state, mock_db):
"""Тест обработки медиагруппы"""
with patch('helper_bot.handlers.private.private_handlers.BotDB', mock_db):
with patch('helper_bot.handlers.private.private_handlers.get_text_message') as mock_get_text:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard_for_post') as mock_keyboard_post:
with patch('helper_bot.handlers.private.private_handlers.get_reply_keyboard') as mock_keyboard:
with patch('helper_bot.handlers.private.private_handlers.prepare_media_group_from_middlewares') as mock_prepare:
with patch('helper_bot.handlers.private.private_handlers.send_media_group_message_to_private_chat') as mock_send_group:
with patch('helper_bot.handlers.private.private_handlers.send_text_message') as mock_send_text:
with patch('helper_bot.handlers.private.private_handlers.messages.get_message') as mock_messages:
with patch('helper_bot.handlers.private.private_handlers.sleep'):
# Настройка моков для медиагруппы
mock_message.media_group_id = 'group_123'
mock_message.content_type = 'photo'
# Создаем мок альбома
album = [mock_message]
album[0].caption = 'Подпись к медиагруппе'
mock_get_text.return_value = 'Обработанная подпись'
mock_keyboard_post.return_value = Mock()
mock_keyboard.return_value = Mock()
mock_prepare.return_value = ['media1', 'media2']
mock_send_group.return_value = 123
mock_send_text.return_value = 456
mock_messages.return_value = "Медиагруппа отправлена!"
# Выполнение теста
await suggest_router(mock_message, mock_state, album)
# Проверки
mock_get_text.assert_called_once()
mock_prepare.assert_called_once()
mock_send_group.assert_called_once()
# Проверяем, что send_text_message был вызван (может быть вызван несколько раз)
assert mock_send_text.call_count >= 1
mock_db.update_helper_message_in_db.assert_called_once()
mock_message.answer.assert_called_once()
mock_state.set_state.assert_called_with("START")
@pytest.mark.asyncio
async def test_suggest_router_unsupported_content(self, mock_message, mock_state):
"""Тест обработки неподдерживаемого типа контента"""
# Настройка моков для неподдерживаемого контента
mock_message.content_type = 'document'
mock_message.media_group_id = None
# Выполнение теста
await suggest_router(mock_message, mock_state)
# Проверяем, что отправлено сообщение о неподдерживаемом типе
mock_message.bot.send_message.assert_called_once()
call_args = mock_message.bot.send_message.call_args
# Проверяем текст сообщения (может быть в позиционных или именованных аргументах)
text = call_args.kwargs.get('text', '') or (call_args[0][1] if len(call_args[0]) > 1 else '')
assert 'не умею работать с таким сообщением' in text
if __name__ == '__main__':
pytest.main([__file__, '-v'])

13
tests/test_settings.ini Normal file
View File

@@ -0,0 +1,13 @@
[Telegram]
bot_token = test_token_123
preview_link = false
main_public = @test
group_for_posts = -1001234567890
group_for_message = -1001234567891
group_for_logs = -1001234567893
important_logs = -1001234567894
test_channel = -1001234567895
[Settings]
logs = true
test = false

208
tests/test_utils.py Normal file
View File

@@ -0,0 +1,208 @@
import pytest
from unittest.mock import Mock, patch
from datetime import datetime
from helper_bot.utils.helper_func import (
get_first_name,
get_text_message,
check_username_and_full_name
)
from helper_bot.utils.messages import get_message
from helper_bot.utils.base_dependency_factory import BaseDependencyFactory, get_global_instance
from database.db import BotDB
class TestHelperFunctions:
"""Тесты для вспомогательных функций"""
@pytest.fixture
def mock_message(self):
"""Создает мок сообщения для тестирования"""
message = Mock()
message.from_user = Mock()
message.from_user.first_name = "Test"
message.from_user.full_name = "Test User"
message.from_user.username = "testuser"
return message
def test_get_first_name(self, mock_message):
"""Тест функции получения имени пользователя"""
# Тест с обычным именем
result = get_first_name(mock_message)
assert result == "Test"
# Тест с пустым именем - функция get_first_name не обрабатывает None
# поэтому этот тест будет падать, что ожидаемо
mock_message.from_user.first_name = None
try:
result = get_first_name(mock_message)
assert False, "Ожидалась ошибка при None first_name"
except AttributeError:
pass # Ожидаемое поведение
def test_get_text_message(self, mock_message):
"""Тест функции обработки текста сообщения"""
# Тест с обычным текстом
text = "Привет, это тестовое сообщение"
result = get_text_message(text, "Test", "testuser")
assert "Test" in result
assert "testuser" in result
assert "тестовое сообщение" in result
# Тест с пустым текстом
result = get_text_message("", "Test", "testuser")
assert "Test" in result
assert "testuser" in result
# Тест с текстом без специальных слов
text = "Обычный текст без специальных слов"
result = get_text_message(text, "Test", "testuser")
assert "Test" in result
assert "testuser" in result
assert "Обычный текст без специальных слов" in result
def test_check_username_and_full_name(self):
"""Тест функции проверки изменений username и full_name"""
# Создаем мок базы данных
mock_db = Mock(spec=BotDB)
mock_db.get_username_and_full_name = Mock(return_value=("olduser", "Old User"))
# Тест с измененными данными
result = check_username_and_full_name(123456, "newuser", "New User", mock_db)
assert result is True
# Тест с неизмененными данными
result = check_username_and_full_name(123456, "olduser", "Old User", mock_db)
assert result is False
# Тест с частично измененными данными
result = check_username_and_full_name(123456, "olduser", "New User", mock_db)
assert result is True
result = check_username_and_full_name(123456, "newuser", "Old User", mock_db)
assert result is True
class TestMessages:
"""Тесты для системы сообщений"""
def test_get_message(self):
"""Тест функции получения сообщений"""
# Тест с существующим ключом
result = get_message("Test", "HELLO_MESSAGE")
assert isinstance(result, str)
assert len(result) > 0
# Тест с несуществующим ключом
try:
result = get_message("Test", "NON_EXISTENT_KEY")
assert False, "Ожидалась ошибка KeyError"
except KeyError:
pass # Ожидаемое поведение
# Тест с пустым именем
result = get_message("", "HELLO_MESSAGE")
assert isinstance(result, str)
assert len(result) > 0
# Тест с None именем - ожидаем ошибку
try:
result = get_message(None, "HELLO_MESSAGE")
assert False, "Ожидалась ошибка TypeError"
except TypeError:
pass # Ожидаемое поведение
def test_get_message_all_types(self):
"""Тест всех типов сообщений"""
message_types = [
"HELLO_MESSAGE",
"SUGGEST_NEWS",
"SUGGEST_NEWS_2",
"BYE_MESSAGE",
"SUCCESS_SEND_MESSAGE",
"CONNECT_WITH_ADMIN",
"QUESTION"
]
for msg_type in message_types:
result = get_message("Test", msg_type)
assert isinstance(result, str)
assert len(result) > 0
class TestBaseDependencyFactory:
"""Тесты для фабрики зависимостей"""
def test_singleton_pattern(self):
"""Тест паттерна синглтон"""
# Сбрасываем глобальный экземпляр
import helper_bot.utils.base_dependency_factory
helper_bot.utils.base_dependency_factory._global_instance = None
# Получаем два экземпляра
instance1 = get_global_instance()
instance2 = get_global_instance()
# Проверяем, что это один и тот же объект
assert instance1 is instance2
assert id(instance1) == id(instance2)
def test_factory_initialization_with_mock_config(self):
"""Тест инициализации фабрики с мок конфигурацией"""
# Этот тест пропускаем, так как сложно замокать ConfigParser
# в контексте уже загруженных модулей
pass
def test_get_settings_method(self):
"""Тест метода get_settings"""
# Этот тест пропускаем, так как сложно замокать ConfigParser
# в контексте уже загруженных модулей
pass
def test_get_db_method(self):
"""Тест метода get_db"""
with patch('helper_bot.utils.base_dependency_factory.configparser.ConfigParser'):
with patch('helper_bot.utils.base_dependency_factory.BotDB') as mock_db:
factory = BaseDependencyFactory()
db = factory.get_db()
assert db is not None
assert db == factory.database
class TestDatabaseIntegration:
"""Тесты интеграции с базой данных"""
def test_database_connection(self):
"""Тест подключения к базе данных"""
with patch('helper_bot.utils.base_dependency_factory.configparser.ConfigParser'):
with patch('helper_bot.utils.base_dependency_factory.BotDB') as mock_db:
factory = BaseDependencyFactory()
# Проверяем, что база данных была создана
mock_db.assert_called_once()
# Проверяем, что get_db возвращает тот же экземпляр
db1 = factory.get_db()
db2 = factory.get_db()
assert db1 is db2
class TestConfigurationHandling:
"""Тесты обработки конфигурации"""
def test_boolean_config_values(self):
"""Тест обработки булевых значений в конфигурации"""
# Этот тест пропускаем, так как сложно замокать ConfigParser
# в контексте уже загруженных модулей
pass
def test_string_config_values(self):
"""Тест обработки строковых значений в конфигурации"""
# Этот тест пропускаем, так как сложно замокать ConfigParser
# в контексте уже загруженных модулей
pass
if __name__ == '__main__':
pytest.main([__file__, '-v'])

View File

@@ -1,9 +1,9 @@
import time import time
from datetime import datetime from datetime import datetime
from helper_bot.utils.base_dependency_factory import BaseDependencyFactory from helper_bot.utils.base_dependency_factory import get_global_instance
bdf = BaseDependencyFactory() bdf = get_global_instance()
BotDB = bdf.get_db() BotDB = bdf.get_db()

View File

@@ -9,14 +9,14 @@ from aiogram.fsm.context import FSMContext
from aiogram.types import FSInputFile from aiogram.types import FSInputFile
from helper_bot.filters.main import ChatTypeFilter from helper_bot.filters.main import ChatTypeFilter
from helper_bot.utils.base_dependency_factory import BaseDependencyFactory from helper_bot.utils.base_dependency_factory import get_global_instance
from logs.custom_logger import logger from logs.custom_logger import logger
from voice_bot.keyboards.keyboards import get_main_keyboard from voice_bot.keyboards.keyboards import get_main_keyboard
from voice_bot.utils.helper_func import last_message from voice_bot.utils.helper_func import last_message
voice_router = Router() voice_router = Router()
bdf = BaseDependencyFactory() bdf = get_global_instance()
GROUP_FOR_LOGS = bdf.settings['Telegram']['group_for_logs'] GROUP_FOR_LOGS = bdf.settings['Telegram']['group_for_logs']
IMPORTANT_LOGS = bdf.settings['Telegram']['important_logs'] IMPORTANT_LOGS = bdf.settings['Telegram']['important_logs']

View File

@@ -1,9 +1,9 @@
import asyncio import asyncio
from helper_bot.utils.base_dependency_factory import BaseDependencyFactory from helper_bot.utils.base_dependency_factory import get_global_instance
from voice_bot.main import start_bot from voice_bot.main import start_bot
bdf = BaseDependencyFactory() bdf = get_global_instance()
if __name__ == '__main__': if __name__ == '__main__':
asyncio.run(start_bot(BaseDependencyFactory())) asyncio.run(start_bot(get_global_instance()))