discordBot/tests/test_commands_pg.py
deadzilla b8b1801f23 fix(weather): исправлены баги в погодном функционале
- JSONDecodeError теперь ловится в fetch_weather (был краш при невалидном JSON от API)
- Порывы ветра больше не теряются при отсутствии base wind (gust fallback)
- Устранено дублирование 'м/с' при порывах + отсутствии направления ветра
- Переименован windspeedKmph → wind_speed_mps (ключ хранил м/с, а не км/ч)
- Обновлены тесты, добавлены 6 новых (gust-only, no-duplication, JSONDecodeError)
2026-07-20 23:56:07 +05:00

280 lines
11 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.

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