discordBot/tests/test_morning_runner.py
deadzilla cef53197b1 feat: !morning — заменить embed на plain text, кот отдельным сообщением
- Переименован format_weather_for_embed -> format_weather_for_message
- Убран markdown жирный (**text**) из заголовков погоды и статей
- Утренний дайджест отправляется как обычное текстовое сообщение
- Котик отправляется отдельным сообщением перед дайджестом
- Truncation через truncate_message (лимит 2000 символов)
- Обновлены тесты: проверка 2 send() вместо embed
2026-07-09 13:28:39 +05:00

246 lines
9.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Тесты для utils/morning_runner.py — Scheduler и run_morning."""
from datetime import datetime
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from utils.morning_runner import Scheduler, run_morning
class TestSchedulerInit:
"""Тесты инициализации Scheduler."""
def test_init_sets_morning_time(self) -> None:
"""Инициализация должна устанавливать время."""
bot = AsyncMock()
with patch.object(Scheduler, "_start_scheduler"):
scheduler = Scheduler(bot, "08:30")
assert scheduler.morning_time == "08:30"
def test_init_default_morning_time(self) -> None:
"""Инициализация с дефолтным временем."""
bot = AsyncMock()
with patch.object(Scheduler, "_start_scheduler"):
scheduler = Scheduler(bot)
assert scheduler.morning_time == "07:00"
def test_init_creates_task(self) -> None:
"""Инициализация должна вызывать _start_scheduler."""
bot = AsyncMock()
with patch.object(Scheduler, "_start_scheduler") as mock_start:
Scheduler(bot)
mock_start.assert_called_once()
class TestSchedulerCalculateNextRun:
"""Тесты расчёта следующего запуска."""
def test_next_run_today_before_time(self) -> None:
"""Если сейчас раньше времени — вернуть сегодня."""
bot = AsyncMock()
with patch.object(Scheduler, "_start_scheduler"):
scheduler = Scheduler(bot, "14:00")
now = datetime(2026, 5, 29, 10, 0, 0)
next_run = scheduler._calculate_next_run(now)
assert next_run == datetime(2026, 5, 29, 14, 0, 0)
def test_next_run_tomorrow_after_time(self) -> None:
"""Если сейчас позже времени — вернуть завтра."""
bot = AsyncMock()
with patch.object(Scheduler, "_start_scheduler"):
scheduler = Scheduler(bot, "14:00")
now = datetime(2026, 5, 29, 15, 0, 0)
next_run = scheduler._calculate_next_run(now)
assert next_run == datetime(2026, 5, 30, 14, 0, 0)
def test_next_run_exact_time(self) -> None:
"""Если сейчас ровно время — вернуть завтра."""
bot = AsyncMock()
with patch.object(Scheduler, "_start_scheduler"):
scheduler = Scheduler(bot, "14:00")
now = datetime(2026, 5, 29, 14, 0, 0)
next_run = scheduler._calculate_next_run(now)
assert next_run == datetime(2026, 5, 30, 14, 0, 0)
class TestSchedulerStartStop:
"""Тесты запуска/остановки планировщика."""
def test_start_starts_task(self) -> None:
"""start() должен вызывать _start_scheduler (1 в __init__ + 1 в start, но реальный task один)."""
bot = AsyncMock()
with patch.object(Scheduler, "_start_scheduler") as mock_start:
scheduler = Scheduler(bot)
scheduler.start()
# __init__ вызывает _start_scheduler, start() тоже вызывает
assert mock_start.call_count == 2
def test_stop_stops_task(self) -> None:
"""stop() должен остановить task."""
bot = AsyncMock()
with patch.object(Scheduler, "_start_scheduler"):
scheduler = Scheduler(bot)
scheduler._running = True
mock_task = MagicMock()
mock_task.done.return_value = False
scheduler._task = mock_task
scheduler.stop()
assert scheduler._running is False
def test_double_start_no_duplicate(self) -> None:
"""Повторный start должен вызывать _start_scheduler дважды (реальный task не дублируется благодаря флагам)."""
bot = AsyncMock()
with patch.object(Scheduler, "_start_scheduler") as mock_start:
scheduler = Scheduler(bot)
scheduler.start() # второй вызов
assert mock_start.call_count == 2
class TestRunMorning:
"""Тесты run_morning."""
@pytest.mark.asyncio
async def test_run_morning_sends_plain_message(self) -> None:
"""run_morning должен отправлять plain text в канал."""
bot = AsyncMock()
channel = AsyncMock()
channel.name = "test-channel"
channel.guild.me = MagicMock()
channel.permissions_for.return_value.send_messages = True
weather_data = {
"current_condition": [
{
"temp_C": "20",
"FeelsLikeC": "22",
"weatherDesc": [{"value": "Clear"}],
"humidity": "50",
"windspeedKmph": "10",
"pressure": "1013",
}
]
}
articles = [
{
"title": "Test",
"link": "http://test.com",
"pub_date": "Mon, 01 Jan 2026 00:00:00 GMT",
"creator": "",
"tags": [],
}
]
posts = [
{
"title": "Test",
"link": "http://test.com",
"pub_date": "Mon, 01 Jan 2026 00:00:00 GMT",
"creator": "",
"tags": [],
}
]
with (
patch(
"utils.morning_runner.fetch_weather",
new=AsyncMock(return_value=weather_data),
),
patch(
"utils.morning_runner.fetch_rss",
new=AsyncMock(side_effect=[articles, posts]),
),
patch(
"utils.morning_runner.fetch_cat",
new=AsyncMock(return_value="http://cat.jpg"),
),
):
await run_morning(bot, channel)
# Два сообщения: кот + дайджест
assert channel.send.call_count == 2
# Первое сообщение — URL кота
first_call = channel.send.call_args_list[0]
assert first_call[0][0] == "http://cat.jpg"
# Второе сообщение — текстовый дайджест
second_call = channel.send.call_args_list[1]
message_text = second_call[0][0]
assert "Утренний дайджест" in message_text
assert "Погода в Магнитогорске" in message_text
class TestRunMorningWithFallback:
"""Тесты fallback в plain text."""
@pytest.mark.asyncio
async def test_run_morning_empty_fallback(self) -> None:
"""run_morning должен отправлять fallback сообщение при пустых данных."""
bot = AsyncMock()
channel = AsyncMock()
channel.name = "test-channel"
channel.guild.me = MagicMock()
channel.permissions_for.return_value.send_messages = True
# Все API возвращают None/пусто
with (
patch(
"utils.morning_runner.fetch_weather", new=AsyncMock(return_value=None)
),
patch("utils.morning_runner.fetch_rss", new=AsyncMock(return_value=None)),
patch("utils.morning_runner.fetch_cat", new=AsyncMock(return_value=None)),
):
await run_morning(bot, channel)
# Только одно сообщение (без кота) с fallback текстом
channel.send.assert_called_once()
call_args = channel.send.call_args
message_text = call_args[0][0]
assert "Не удалось получить данные из внешних источников" in message_text
@pytest.mark.asyncio
async def test_run_morning_only_weather_data(self) -> None:
"""run_morning должен корректно обрабатывать только погоду без новостей."""
bot = AsyncMock()
channel = AsyncMock()
channel.name = "test-channel"
channel.guild.me = MagicMock()
channel.permissions_for.return_value.send_messages = True
weather_data = {
"current_condition": [
{
"temp_C": "20",
"FeelsLikeC": "22",
"weatherDesc": [{"value": "Clear"}],
"humidity": "50",
"windspeedKmph": "10",
"pressure": "1013",
}
]
}
with (
patch(
"utils.morning_runner.fetch_weather",
new=AsyncMock(return_value=weather_data),
),
patch(
"utils.morning_runner.fetch_rss",
new=AsyncMock(side_effect=[None, None]),
),
patch("utils.morning_runner.fetch_cat", new=AsyncMock(return_value=None)),
):
await run_morning(bot, channel)
# Одно сообщение (погода + отсутствие новостей, без кота)
channel.send.assert_called_once()
call_args = channel.send.call_args
message_text = call_args[0][0]
# Проверяем, что в тексте есть погода и нет fallback сообщения
assert "Погода в Магнитогорске" in message_text
assert (
"Не удалось получить данные из внешних источников" not in message_text
)