"""Тесты для utils/morning_runner.py — Scheduler и run_morning.""" import asyncio from datetime import 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() mock_loop = MagicMock() with patch("utils.morning_runner.tasks.loop", return_value=mock_loop): scheduler = Scheduler(bot, "08:30") assert scheduler.morning_time == "08:30" def test_init_default_morning_time(self): """Инициализация с дефолтным временем.""" bot = AsyncMock() mock_loop = MagicMock() with patch("utils.morning_runner.tasks.loop", return_value=mock_loop): scheduler = Scheduler(bot) assert scheduler.morning_time == "07:00" def test_init_creates_loop(self): """Инициализация должна создавать loop.""" bot = AsyncMock() mock_loop = MagicMock() with patch("utils.morning_runner.tasks.loop", return_value=mock_loop): scheduler = Scheduler(bot) assert scheduler.morning_loop is not None class TestSchedulerCalculateNextRun: """Тесты расчёта следующего запуска.""" @pytest.fixture(autouse=True) def _mock_loop(self): """Замокать tasks.loop, чтобы не создавать реальный coroutine.""" with patch("utils.morning_runner.tasks.loop", return_value=MagicMock()): yield def test_next_run_today_before_time(self): """Если сейчас раньше времени — вернуть сегодня.""" bot = AsyncMock() scheduler = Scheduler(bot, "14:00") with patch("utils.morning_runner.datetime") as mock_dt: mock_dt.now.return_value = datetime(2026, 5, 29, 10, 0, 0) next_run = scheduler._calculate_next_run() assert next_run == datetime(2026, 5, 29, 14, 0, 0) def test_next_run_tomorrow_after_time(self): """Если сейчас позже времени — вернуть завтра.""" bot = AsyncMock() scheduler = Scheduler(bot, "14:00") with patch("utils.morning_runner.datetime") as mock_dt: mock_dt.now.return_value = datetime(2026, 5, 29, 15, 0, 0) next_run = scheduler._calculate_next_run() assert next_run == datetime(2026, 5, 30, 14, 0, 0) def test_next_run_exact_time(self): """Если сейчас ровно время — вернуть завтра.""" bot = AsyncMock() scheduler = Scheduler(bot, "14:00") with patch("utils.morning_runner.datetime") as mock_dt: mock_dt.now.return_value = datetime(2026, 5, 29, 14, 0, 0) next_run = scheduler._calculate_next_run() assert next_run == datetime(2026, 5, 30, 14, 0, 0) class TestSchedulerStartStop: """Тесты запуска/остановки планировщика.""" @pytest.fixture(autouse=True) def _mock_loop(self): """Замокать tasks.loop, чтобы не создавать реальный coroutine.""" with patch("utils.morning_runner.tasks.loop", return_value=MagicMock()): yield def test_start_starts_loop(self): """start() должен вызывать start() на loop.""" bot = AsyncMock() scheduler = Scheduler(bot) loop_mock = MagicMock() scheduler.morning_loop = loop_mock scheduler.start() loop_mock.start.assert_called_once() def test_stop_stops_loop(self): """stop() должен вызывать stop() на loop.""" bot = AsyncMock() scheduler = Scheduler(bot) loop_mock = MagicMock() scheduler.morning_loop = loop_mock scheduler.stop() loop_mock.stop.assert_called_once() class TestSchedulerCheckAndRun: """Тесты проверки и запуска morning.""" @pytest.mark.asyncio async def test_check_and_run_same_day_no_duplicate(self): """Не должен запускать дважды в один день.""" bot = AsyncMock() scheduler = Scheduler(bot, "07:00") scheduler._last_run_date = datetime.now().day with patch("utils.morning_runner.datetime") as mock_dt: mock_dt.now.return_value = datetime(2026, 5, 29, 7, 0, 0) await scheduler._check_and_run_morning() # run_morning не должен вызываться assert scheduler._last_run_date == datetime.now().day 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