"""Тесты для utils/morning_runner.py — Scheduler и run_morning.""" import asyncio from datetime import date, datetime from unittest.mock import AsyncMock, MagicMock, patch, PropertyMock import discord import pytest from utils.morning_runner import Scheduler, run_morning class TestSchedulerInit: """Тесты инициализации Scheduler.""" def test_init_sets_morning_time(self): """Инициализация должна устанавливать время.""" bot = AsyncMock() with patch("asyncio.create_task") as mock_task: scheduler = Scheduler(bot, "08:30") assert scheduler.morning_time == "08:30" def test_init_default_morning_time(self): """Инициализация с дефолтным временем.""" bot = AsyncMock() with patch("asyncio.create_task") as mock_task: scheduler = Scheduler(bot) assert scheduler.morning_time == "07:00" def test_init_creates_task(self): """Инициализация должна создавать asyncio.Task.""" bot = AsyncMock() with patch("asyncio.create_task") as mock_task: scheduler = Scheduler(bot) mock_task.assert_called_once() assert scheduler._task is not None class TestSchedulerCalculateNextRun: """Тесты расчёта следующего запуска.""" def test_next_run_today_before_time(self): """Если сейчас раньше времени — вернуть сегодня.""" bot = AsyncMock() with patch("asyncio.create_task"): 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): """Если сейчас позже времени — вернуть завтра.""" bot = AsyncMock() with patch("asyncio.create_task"): 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): """Если сейчас ровно время — вернуть завтра.""" bot = AsyncMock() with patch("asyncio.create_task"): 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): """start() должен запустить task.""" bot = AsyncMock() with patch("asyncio.create_task"): scheduler = Scheduler(bot) assert scheduler._running is True def test_stop_stops_task(self): """stop() должен остановить task.""" bot = AsyncMock() with patch("asyncio.create_task"): scheduler = Scheduler(bot) scheduler.stop() assert scheduler._running is False def test_double_start_no_duplicate(self): """Повторный start не должен создавать второй task.""" bot = AsyncMock() with patch("asyncio.create_task") as mock_task: scheduler = Scheduler(bot) scheduler.start() # второй вызов # create_task вызван только при инициализации assert mock_task.call_count == 1 class TestRunMorning: """Тесты run_morning.""" @pytest.mark.asyncio async def test_run_morning_sends_embed(self): """run_morning должен отправлять embed в канал.""" 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")), \ patch("utils.morning_runner.discord.Embed") as mock_embed: await run_morning(bot, channel) channel.send.assert_called_once() call_args = channel.send.call_args[1] assert "embed" in call_args assert call_args["embed"] is not None class TestRunMorningWithFallback: """Тесты fallback в пустом embed.""" @pytest.mark.asyncio async def test_run_morning_empty_embed_fallback(self): """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)), \ patch("utils.morning_runner.discord.Embed") as mock_embed_class: embed_mock = AsyncMock() mock_embed_class.return_value = embed_mock await run_morning(bot, channel) # Убедимся, что send был вызван channel.send.assert_called_once() call_args = channel.send.call_args[1] assert "embed" in call_args # Проверяем, что description содержит fallback сообщение embed_description = call_args["embed"].description assert "Не удалось получить данные из внешних источников" in embed_description @pytest.mark.asyncio async def test_run_morning_only_weather_data(self): """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[1] assert "embed" in call_args # Проверяем, что в embed есть только погода и нет fallback сообщения embed_description = call_args["embed"].description assert "Погода в Магнитогорске" in embed_description assert "Не удалось получить данные из внешних источников" not in embed_description