ruff format --check падает на 7 файлах (не 5 как было указано). Изменения — чистое форматирование: line wrapping, trailing commas, blank lines. Никаких логических изменений. Файлы: bot.py, tests/test_commands_news.py, tests/test_commands_pg.py, tests/test_fetch_weather.py, tests/test_help_command.py, tests/test_morning_runner.py, utils/pogoda.py
284 lines
11 KiB
Python
284 lines
11 KiB
Python
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
|