import pytest from unittest.mock import AsyncMock, MagicMock, patch from commands.pg import Pg class TestPgInit: """Тесты инициализации Cog Pg.""" def test_cog_initialized(self) -> None: """Cog инициализируется без ошибок.""" cog = Pg() assert cog is not None class TestPgCommand: """Тесты команды !pg.""" def _make_cog(self): return Pg() def _make_ctx(self, send_return=None): ctx = MagicMock() ctx.send = AsyncMock(return_value=send_return) return ctx def _make_weather_data(self, **extra): """Создать mock weather data (Яндекс Погода формат). wind_speed_mps — скорость ветра в м/с от Яндекса. wind_gust — порывы ветра в м/с. wind_dir — направление ветра. pressure — уже в мм рт. ст. weatherDesc — на русском (из yandex_condition_to_russian). """ defaults = { "current_condition": [ { "temp_C": 22, "FeelsLikeC": 24, "weatherDesc": [{"value": "Ясно"}], "humidity": 45, "wind_speed_mps": 5.0, # м/с "wind_gust": 8.0, "wind_dir": "n", "pressure": 735.0, } ] } defaults["current_condition"][0].update(extra) return defaults @pytest.mark.asyncio async def test_pg_success(self) -> None: """Успешный запрос погоды должен отправить сообщение с данными.""" cog = self._make_cog() ctx = self._make_ctx() weather = self._make_weather_data() with patch("commands.pg.fetch_weather", new=AsyncMock(return_value=weather)): await cog.pg.callback(cog, ctx) ctx.send.assert_called_once() args = ctx.send.call_args[0][0] assert "Температура: 22°C" in args assert "(ощущается как 24°C)" in args assert "Описание: Ясно" in args assert "Влажность: 45%" in args assert "Ветер: 5.0 (порывы 8.0), северный м/с" in args assert "Давление: 735.0 мм рт. ст." in args @pytest.mark.asyncio async def test_pg_fetch_returns_none(self) -> None: """fetch_weather вернул None — бот должен сообщить об ошибке.""" cog = self._make_cog() ctx = self._make_ctx() with patch("commands.pg.fetch_weather", new=AsyncMock(return_value=None)): await cog.pg.callback(cog, ctx) ctx.send.assert_called_once_with("Не удалось получить данные о погоде.") @pytest.mark.asyncio async def test_pg_empty_current_condition(self) -> None: """current_condition пустой список — graceful fallback.""" cog = self._make_cog() ctx = self._make_ctx() weather = {"current_condition": []} with patch("commands.pg.fetch_weather", new=AsyncMock(return_value=weather)): await cog.pg.callback(cog, ctx) ctx.send.assert_called_once() assert "Не удалось получить данные о погоде" in ctx.send.call_args[0][0] @pytest.mark.asyncio async def test_pg_current_condition_none(self) -> None: """current_condition — пустой dict — бот сообщает об ошибке (empty dict is falsy).""" cog = self._make_cog() ctx = self._make_ctx() weather = {"current_condition": [{}]} with patch("commands.pg.fetch_weather", new=AsyncMock(return_value=weather)): await cog.pg.callback(cog, ctx) ctx.send.assert_called_once_with("Не удалось получить данные о погоде.") @pytest.mark.asyncio async def test_pg_wind_non_numeric(self) -> None: """wind_speed_mps — не число — показываются порывы ветра (gust fallback).""" cog = self._make_cog() ctx = self._make_ctx() weather = self._make_weather_data( wind_speed_mps="abc" ) # gust=8.0, dir=n по умолчанию with patch("commands.pg.fetch_weather", new=AsyncMock(return_value=weather)): await cog.pg.callback(cog, ctx) args = ctx.send.call_args[0][0] # base wind невалиден → показываем порывы assert "Ветер: порывы 8.0, северный м/с" in args @pytest.mark.asyncio async def test_pg_wind_none(self) -> None: """wind_speed_mps отсутствует, порывов нет — wind должен быть '— м/с'.""" cog = self._make_cog() ctx = self._make_ctx() weather = self._make_weather_data( wind_speed_mps=None, wind_gust=None, wind_dir=None ) with patch("commands.pg.fetch_weather", new=AsyncMock(return_value=weather)): await cog.pg.callback(cog, ctx) args = ctx.send.call_args[0][0] assert "Ветер: — м/с" in args @pytest.mark.asyncio async def test_pg_zero_wind(self) -> None: """wind_speed_mps = 0 — 0.0 + порывы + направление.""" cog = self._make_cog() ctx = self._make_ctx() weather = self._make_weather_data( wind_speed_mps=0 ) # gust=8.0, dir=n по умолчанию with patch("commands.pg.fetch_weather", new=AsyncMock(return_value=weather)): await cog.pg.callback(cog, ctx) args = ctx.send.call_args[0][0] assert "Ветер: 0.0 (порывы 8.0), северный м/с" in args @pytest.mark.asyncio async def test_pg_default_values(self) -> None: """Поля с отсутствующими значениями должны давать '—'.""" cog = self._make_cog() ctx = self._make_ctx() weather = self._make_weather_data( temp_C=None, FeelsLikeC=None, weatherDesc=[{"value": None}], humidity=None, pressure=None, ) with patch("commands.pg.fetch_weather", new=AsyncMock(return_value=weather)): await cog.pg.callback(cog, ctx) args = ctx.send.call_args[0][0] # None значения корректно заменяются на "—" assert "Температура: —°C" in args assert "ощущается как —°C" in args assert "Описание: —" in args assert "Влажность: —%" in args assert "Давление: — мм рт. ст." in args @pytest.mark.asyncio async def test_pg_russian_weather_description(self) -> None: """Описание погоды на русском должно корректно отображаться.""" cog = self._make_cog() ctx = self._make_ctx() weather = self._make_weather_data( weatherDesc=[{"value": "Переменная облачность"}] ) with patch("commands.pg.fetch_weather", new=AsyncMock(return_value=weather)): await cog.pg.callback(cog, ctx) args = ctx.send.call_args[0][0] assert "Описание: Переменная облачность" in args @pytest.mark.asyncio async def test_pg_with_wind_gust(self) -> None: """Порывы ветра должны отображаться.""" cog = self._make_cog() ctx = self._make_ctx() weather = self._make_weather_data( wind_speed_mps=3.0, wind_gust=10.5, wind_dir="n", ) with patch("commands.pg.fetch_weather", new=AsyncMock(return_value=weather)): await cog.pg.callback(cog, ctx) args = ctx.send.call_args[0][0] assert "Ветер: 3.0 (порывы 10.5), северный м/с" in args @pytest.mark.asyncio async def test_pg_with_wind_direction(self) -> None: """Направление ветра должно переводиться.""" cog = self._make_cog() ctx = self._make_ctx() weather = self._make_weather_data( wind_speed_mps=5.0, wind_gust=8.0, wind_dir="se", ) with patch("commands.pg.fetch_weather", new=AsyncMock(return_value=weather)): await cog.pg.callback(cog, ctx) args = ctx.send.call_args[0][0] assert "юго-восточный" in args @pytest.mark.asyncio async def test_pg_float_values(self) -> None: """Float значения из Яндекс Погоды должны работать.""" cog = self._make_cog() ctx = self._make_ctx() weather = self._make_weather_data( temp_C=17.3, FeelsLikeC=16.8, humidity=87.5, wind_speed_mps=2.1, # м/с wind_gust=5.5, wind_dir="n", pressure=750.1, ) with patch("commands.pg.fetch_weather", new=AsyncMock(return_value=weather)): await cog.pg.callback(cog, ctx) args = ctx.send.call_args[0][0] assert "Температура: 17.3°C" in args assert "Ветер: 2.1 (порывы 5.5), северный м/с" in args assert "Давление: 750.1 мм рт. ст." in args @pytest.mark.asyncio async def test_pg_gust_only_no_base_wind(self) -> None: """При отсутствии base wind порывы ветра всё равно показываются.""" cog = self._make_cog() ctx = self._make_ctx() weather = self._make_weather_data( wind_speed_mps=None, wind_gust=10.5, wind_dir="n", ) with patch("commands.pg.fetch_weather", new=AsyncMock(return_value=weather)): await cog.pg.callback(cog, ctx) args = ctx.send.call_args[0][0] assert "Ветер: порывы 10.5, северный м/с" in args @pytest.mark.asyncio async def test_pg_gust_only_no_direction(self) -> None: """Порывы ветра без направления и base wind — без дублирования 'м/с'.""" cog = self._make_cog() ctx = self._make_ctx() weather = self._make_weather_data( wind_speed_mps=None, wind_gust=10.5, wind_dir=None, ) with patch("commands.pg.fetch_weather", new=AsyncMock(return_value=weather)): await cog.pg.callback(cog, ctx) args = ctx.send.call_args[0][0] assert "Ветер: порывы 10.5 м/с" in args # Убеждаемся, что 'м/с' не дублируется assert args.count("м/с") == 1