Исправление: добавил -> None ко всем 140 test-функциям
This commit is contained in:
parent
ea40400a65
commit
beae42fdc8
@ -19,7 +19,7 @@ sys.path.insert(0, str(ROOT_DIR))
|
||||
class TestBotInit:
|
||||
"""Тесты для инициализации бота."""
|
||||
|
||||
def test_bot_created_with_default_prefix(self):
|
||||
def test_bot_created_with_default_prefix(self) -> None:
|
||||
"""Проверка, что бот создан с правильным префиксом команд."""
|
||||
import bot
|
||||
|
||||
@ -35,7 +35,7 @@ class TestBotInit:
|
||||
class TestBotErrorHandling:
|
||||
"""Тесты для проверки обработки ошибок запуска бота."""
|
||||
|
||||
def test_bot_handles_login_failure(self):
|
||||
def test_bot_handles_login_failure(self) -> None:
|
||||
"""BotRunner.run() обрабатывает discord.LoginFailure."""
|
||||
import bot
|
||||
import discord
|
||||
@ -48,7 +48,7 @@ class TestBotErrorHandling:
|
||||
runner.run("fake_token")
|
||||
mock_exit.assert_called_once_with(1)
|
||||
|
||||
def test_bot_handles_http_exception(self):
|
||||
def test_bot_handles_http_exception(self) -> None:
|
||||
"""BotRunner.run() обрабатывает discord.HTTPException."""
|
||||
import bot
|
||||
import discord
|
||||
@ -62,7 +62,7 @@ class TestBotErrorHandling:
|
||||
runner.run("fake_token")
|
||||
mock_exit.assert_called_once_with(1)
|
||||
|
||||
def test_shutdown_uses_on_shutdown_listener(self):
|
||||
def test_shutdown_uses_on_shutdown_listener(self) -> None:
|
||||
"""BotRunner.run() регистрирует on_shutdown вместо signal handlers.
|
||||
|
||||
Signal handlers с asyncio.new_event_loop() создают race condition
|
||||
@ -81,7 +81,7 @@ class TestBotErrorHandling:
|
||||
content = f.read()
|
||||
assert "signal.signal" not in content, "Не должно быть signal.signal — используется on_shutdown"
|
||||
|
||||
def test_code_uses_async_bot_pattern(self):
|
||||
def test_code_uses_async_bot_pattern(self) -> None:
|
||||
"""Проверка, что bot.py использует async with / asyncio.run."""
|
||||
with open(ROOT_DIR / "bot.py", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
@ -7,7 +7,7 @@ from commands.pg import Pg
|
||||
class TestPgInit:
|
||||
"""Тесты инициализации Cog Pg."""
|
||||
|
||||
def test_init_sets_api_url(self):
|
||||
def test_init_sets_api_url(self) -> None:
|
||||
"""__init__ должен устанавливать api_url."""
|
||||
cog = Pg()
|
||||
assert cog.api_url == "https://wttr.in/Magnitogorsk?format=j1&lang=ru"
|
||||
@ -42,7 +42,7 @@ class TestPgCommand:
|
||||
return defaults
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pg_success(self):
|
||||
async def test_pg_success(self) -> None:
|
||||
"""Успешный запрос погоды должен отправить embed с данными."""
|
||||
cog = self._make_cog()
|
||||
ctx = self._make_ctx()
|
||||
@ -61,7 +61,7 @@ class TestPgCommand:
|
||||
assert "Давление: 759.8 мм рт. ст." in args
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pg_fetch_returns_none(self):
|
||||
async def test_pg_fetch_returns_none(self) -> None:
|
||||
"""fetch_weather вернул None — бот должен сообщить об ошибке."""
|
||||
cog = self._make_cog()
|
||||
ctx = self._make_ctx()
|
||||
@ -72,7 +72,7 @@ class TestPgCommand:
|
||||
ctx.send.assert_called_once_with("Не удалось получить данные о погоде.")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pg_empty_current_condition(self):
|
||||
async def test_pg_empty_current_condition(self) -> None:
|
||||
"""current_condition пустой список — graceful fallback."""
|
||||
cog = self._make_cog()
|
||||
ctx = self._make_ctx()
|
||||
@ -84,7 +84,7 @@ class TestPgCommand:
|
||||
assert "Не удалось получить данные о погоде" in ctx.send.call_args[0][0]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pg_current_condition_none(self):
|
||||
async def test_pg_current_condition_none(self) -> None:
|
||||
"""current_condition — пустой dict — бот должен сообщить об ошибке."""
|
||||
cog = self._make_cog()
|
||||
ctx = self._make_ctx()
|
||||
@ -96,7 +96,7 @@ class TestPgCommand:
|
||||
ctx.send.assert_called_once_with("Не удалось получить данные о погоде.")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pg_wind_non_numeric(self):
|
||||
async def test_pg_wind_non_numeric(self) -> None:
|
||||
"""windspeedKmph — не число — wind должен быть '—'."""
|
||||
cog = self._make_cog()
|
||||
ctx = self._make_ctx()
|
||||
@ -109,7 +109,7 @@ class TestPgCommand:
|
||||
assert "Ветер: — м/с" in args
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pg_wind_none(self):
|
||||
async def test_pg_wind_none(self) -> None:
|
||||
"""windspeedKmph отсутствует — wind должен быть '—'."""
|
||||
cog = self._make_cog()
|
||||
ctx = self._make_ctx()
|
||||
@ -122,7 +122,7 @@ class TestPgCommand:
|
||||
assert "Ветер: — м/с" in args
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pg_zero_wind(self):
|
||||
async def test_pg_zero_wind(self) -> None:
|
||||
"""windspeedKmph = 0 — wind должен быть 0.0."""
|
||||
cog = self._make_cog()
|
||||
ctx = self._make_ctx()
|
||||
@ -135,7 +135,7 @@ class TestPgCommand:
|
||||
assert "Ветер: 0.0 м/с" in args
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pg_default_values(self):
|
||||
async def test_pg_default_values(self) -> None:
|
||||
"""Поля с отсутствующими значениями должны давать '—'."""
|
||||
cog = self._make_cog()
|
||||
ctx = self._make_ctx()
|
||||
@ -159,7 +159,7 @@ class TestPgCommand:
|
||||
assert "Давление: — мм рт. ст." in args
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pg_translate_unknown_weather(self):
|
||||
async def test_pg_translate_unknown_weather(self) -> None:
|
||||
"""Неизвестное описание погоды должно возвращать оригинал."""
|
||||
cog = self._make_cog()
|
||||
ctx = self._make_ctx()
|
||||
@ -172,7 +172,7 @@ class TestPgCommand:
|
||||
assert "Описание: UnknownXYZ" in args
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pg_russian_weather_description(self):
|
||||
async def test_pg_russian_weather_description(self) -> None:
|
||||
"""Описание погоды на русском должно корректно переводиться."""
|
||||
cog = self._make_cog()
|
||||
ctx = self._make_ctx()
|
||||
@ -185,7 +185,7 @@ class TestPgCommand:
|
||||
assert "Описание: Переменная облачность" in args
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pg_negative_pressure(self):
|
||||
async def test_pg_negative_pressure(self) -> None:
|
||||
"""Отрицательное давление должно конвертироваться."""
|
||||
cog = self._make_cog()
|
||||
ctx = self._make_ctx()
|
||||
@ -198,7 +198,7 @@ class TestPgCommand:
|
||||
assert "Давление: -37.5 мм рт. ст." in args
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pg_high_wind(self):
|
||||
async def test_pg_high_wind(self) -> None:
|
||||
"""Большая скорость ветра должна корректно округляться."""
|
||||
cog = self._make_cog()
|
||||
ctx = self._make_ctx()
|
||||
|
||||
@ -13,7 +13,7 @@ class TestStatsCommand:
|
||||
guild.member_count = member_count
|
||||
return guild
|
||||
|
||||
async def test_stats_sends_embed(self):
|
||||
async def test_stats_sends_embed(self) -> None:
|
||||
"""Команда stats отправляет embed-сообщение."""
|
||||
from commands.stats import Stats
|
||||
|
||||
@ -30,7 +30,7 @@ class TestStatsCommand:
|
||||
embed = call_args[1]["embed"] if call_args[1] else call_args[0][0]
|
||||
assert embed.title == "Статистика серверов"
|
||||
|
||||
async def test_stats_correct_values(self):
|
||||
async def test_stats_correct_values(self) -> None:
|
||||
"""Значения серверов, каналов и пользователей считаются верно."""
|
||||
from commands.stats import Stats
|
||||
|
||||
@ -54,7 +54,7 @@ class TestStatsCommand:
|
||||
assert fields["Пользователей"] == "250"
|
||||
assert "35.0 мс" in fields["Пинг"]
|
||||
|
||||
async def test_stats_empty_guilds(self):
|
||||
async def test_stats_empty_guilds(self) -> None:
|
||||
"""Пустой список серверов не вызывает ошибок."""
|
||||
from commands.stats import Stats
|
||||
|
||||
@ -73,7 +73,7 @@ class TestStatsCommand:
|
||||
assert fields["Каналов"] == "0"
|
||||
assert fields["Пользователей"] == "0"
|
||||
|
||||
async def test_stats_none_member_count(self):
|
||||
async def test_stats_none_member_count(self) -> None:
|
||||
"""member_count=None не вызывает ошибок."""
|
||||
from commands.stats import Stats
|
||||
|
||||
@ -92,7 +92,7 @@ class TestStatsCommand:
|
||||
fields = {f.name: f.value for f in embed.fields}
|
||||
assert fields["Пользователей"] == "0"
|
||||
|
||||
async def test_stats_excludes_categories(self):
|
||||
async def test_stats_excludes_categories(self) -> None:
|
||||
"""Категории не входят в счётчик каналов."""
|
||||
import discord
|
||||
from commands.stats import Stats
|
||||
|
||||
@ -8,7 +8,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
class TestStatusCommand:
|
||||
"""Тесты Discord-команды status."""
|
||||
|
||||
async def test_status_sends_embed(self):
|
||||
async def test_status_sends_embed(self) -> None:
|
||||
"""Команда status отправляет embed-сообщение."""
|
||||
from commands.status import Status
|
||||
|
||||
@ -26,7 +26,7 @@ class TestStatusCommand:
|
||||
assert embed.title == "Статус бота"
|
||||
assert "42.0 мс" in embed.fields[0].value
|
||||
|
||||
async def test_status_uptime_format(self):
|
||||
async def test_status_uptime_format(self) -> None:
|
||||
"""Uptime форматируется корректно."""
|
||||
from commands.status import Status
|
||||
|
||||
@ -50,20 +50,20 @@ class TestStatusCommand:
|
||||
class TestFormatUptime:
|
||||
"""Тесты форматирования uptime."""
|
||||
|
||||
def test_zero_seconds(self):
|
||||
def test_zero_seconds(self) -> None:
|
||||
from commands.status import Status
|
||||
|
||||
result = Status._format_uptime(0)
|
||||
assert result == "0с"
|
||||
|
||||
def test_minutes_and_seconds(self):
|
||||
def test_minutes_and_seconds(self) -> None:
|
||||
from commands.status import Status
|
||||
|
||||
result = Status._format_uptime(125) # 2м 5с
|
||||
assert "2м" in result
|
||||
assert "5с" in result
|
||||
|
||||
def test_hours_minutes_seconds(self):
|
||||
def test_hours_minutes_seconds(self) -> None:
|
||||
from commands.status import Status
|
||||
|
||||
result = Status._format_uptime(3661) # 1ч 1м 1с
|
||||
@ -71,7 +71,7 @@ class TestFormatUptime:
|
||||
assert "1м" in result
|
||||
assert "1с" in result
|
||||
|
||||
def test_full_day(self):
|
||||
def test_full_day(self) -> None:
|
||||
from commands.status import Status
|
||||
|
||||
result = Status._format_uptime(90061) # 1д 1ч 1м 1с
|
||||
|
||||
@ -8,7 +8,7 @@ class TestFetchCat:
|
||||
"""Тесты функции fetch_cat() — получение URL случайного котика."""
|
||||
|
||||
@patch("utils.cat._session.get")
|
||||
async def test_fetch_cat_success(self, mock_get):
|
||||
async def test_fetch_cat_success(self, mock_get) -> None:
|
||||
"""Успешный ответ с URL должен вернуть строку."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = [{"url": "https://example.com/cat.jpg"}]
|
||||
@ -18,7 +18,7 @@ class TestFetchCat:
|
||||
assert result == "https://example.com/cat.jpg"
|
||||
|
||||
@patch("utils.cat._session.get")
|
||||
async def test_fetch_cat_empty_array(self, mock_get):
|
||||
async def test_fetch_cat_empty_array(self, mock_get) -> None:
|
||||
"""Пустой массив должен вернуть None."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = []
|
||||
@ -28,7 +28,7 @@ class TestFetchCat:
|
||||
assert result is None
|
||||
|
||||
@patch("utils.cat._session.get")
|
||||
async def test_fetch_cat_http_error(self, mock_get):
|
||||
async def test_fetch_cat_http_error(self, mock_get) -> None:
|
||||
"""HTTP-ошибка (raise_for_status) должна вернуть None."""
|
||||
import requests
|
||||
mock_response = MagicMock()
|
||||
@ -38,7 +38,7 @@ class TestFetchCat:
|
||||
assert result is None
|
||||
|
||||
@patch("utils.cat._session.get")
|
||||
async def test_fetch_cat_connection_error(self, mock_get):
|
||||
async def test_fetch_cat_connection_error(self, mock_get) -> None:
|
||||
"""ConnectionError должна вернуть None."""
|
||||
from requests.exceptions import ConnectionError
|
||||
mock_get.side_effect = ConnectionError("No connection")
|
||||
@ -46,7 +46,7 @@ class TestFetchCat:
|
||||
assert result is None
|
||||
|
||||
@patch("utils.cat._session.get")
|
||||
async def test_fetch_cat_timeout(self, mock_get):
|
||||
async def test_fetch_cat_timeout(self, mock_get) -> None:
|
||||
"""Timeout должна вернуть None."""
|
||||
from requests.exceptions import Timeout
|
||||
mock_get.side_effect = Timeout("Request timed out")
|
||||
@ -54,7 +54,7 @@ class TestFetchCat:
|
||||
assert result is None
|
||||
|
||||
@patch("utils.cat._session.get")
|
||||
async def test_fetch_cat_ssl_error(self, mock_get):
|
||||
async def test_fetch_cat_ssl_error(self, mock_get) -> None:
|
||||
"""SSLError должна вернуть None."""
|
||||
from requests.exceptions import SSLError
|
||||
mock_get.side_effect = SSLError("SSL handshake failed")
|
||||
@ -62,7 +62,7 @@ class TestFetchCat:
|
||||
assert result is None
|
||||
|
||||
@patch("utils.cat._session.get")
|
||||
async def test_fetch_cat_json_parse_error(self, mock_get):
|
||||
async def test_fetch_cat_json_parse_error(self, mock_get) -> None:
|
||||
"""Ошибка парсинга JSON должна вернуть None."""
|
||||
import requests
|
||||
mock_response = MagicMock()
|
||||
@ -73,7 +73,7 @@ class TestFetchCat:
|
||||
assert result is None
|
||||
|
||||
@patch("utils.cat._session.get")
|
||||
async def test_fetch_cat_missing_url_key(self, mock_get):
|
||||
async def test_fetch_cat_missing_url_key(self, mock_get) -> None:
|
||||
"""Отсутствие ключа 'url' в ответе должно вернуть None."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = [{"error": "no image"}]
|
||||
@ -83,7 +83,7 @@ class TestFetchCat:
|
||||
assert result is None
|
||||
|
||||
@patch("utils.cat._session.get")
|
||||
async def test_fetch_cat_request_exception(self, mock_get):
|
||||
async def test_fetch_cat_request_exception(self, mock_get) -> None:
|
||||
"""Общий RequestException должен вернуть None."""
|
||||
import requests
|
||||
mock_get.side_effect = requests.RequestException("Generic error")
|
||||
@ -91,7 +91,7 @@ class TestFetchCat:
|
||||
assert result is None
|
||||
|
||||
@patch("utils.cat._session.get")
|
||||
async def test_fetch_cat_url_with_special_chars(self, mock_get):
|
||||
async def test_fetch_cat_url_with_special_chars(self, mock_get) -> None:
|
||||
"""URL со спецсимволами должен вернуться как есть."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = [{"url": "https://example.com/cat?w=100&h=200"}]
|
||||
|
||||
@ -8,7 +8,7 @@ class TestFetchRss:
|
||||
"""Тесты функции fetch_rss() — получение и парсинг RSS-ленты."""
|
||||
|
||||
@patch("utils.news._session.get")
|
||||
async def test_fetch_rss_success_rss20(self, mock_get):
|
||||
async def test_fetch_rss_success_rss20(self, mock_get) -> None:
|
||||
"""Успешный ответ RSS 2.0 должен вернуть список статей."""
|
||||
rss_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0">
|
||||
@ -47,7 +47,7 @@ class TestFetchRss:
|
||||
assert result[1]["tags"] == []
|
||||
|
||||
@patch("utils.news._session.get")
|
||||
async def test_fetch_rss_success_atom(self, mock_get):
|
||||
async def test_fetch_rss_success_atom(self, mock_get) -> None:
|
||||
"""Успешный ответ Atom должен вернуть список статей."""
|
||||
atom_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
@ -73,7 +73,7 @@ class TestFetchRss:
|
||||
assert result[0]["tags"] == ["AI"]
|
||||
|
||||
@patch("utils.news._session.get")
|
||||
async def test_fetch_rss_empty_items(self, mock_get):
|
||||
async def test_fetch_rss_empty_items(self, mock_get) -> None:
|
||||
"""RSS без items должен вернуть пустой список."""
|
||||
rss_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0">
|
||||
@ -88,7 +88,7 @@ class TestFetchRss:
|
||||
assert result == []
|
||||
|
||||
@patch("utils.news._session.get")
|
||||
async def test_fetch_rss_no_matching_format(self, mock_get):
|
||||
async def test_fetch_rss_no_matching_format(self, mock_get) -> None:
|
||||
"""Неизвестный формат XML должен вернуть пустой список."""
|
||||
xml_content = """<?xml version="1.0"?>
|
||||
<unknown></unknown>""".encode()
|
||||
@ -100,7 +100,7 @@ class TestFetchRss:
|
||||
assert result == []
|
||||
|
||||
@patch("utils.news._session.get")
|
||||
async def test_fetch_rss_missing_title(self, mock_get):
|
||||
async def test_fetch_rss_missing_title(self, mock_get) -> None:
|
||||
"""Статья без title должна получить 'Без названия'."""
|
||||
rss_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0">
|
||||
@ -125,7 +125,7 @@ class TestFetchRss:
|
||||
assert result[0]["tags"] == []
|
||||
|
||||
@patch("utils.news._session.get")
|
||||
async def test_fetch_rss_missing_guid(self, mock_get):
|
||||
async def test_fetch_rss_missing_guid(self, mock_get) -> None:
|
||||
"""Статья без guid isPermaLink должна иметь пустую ссылку."""
|
||||
rss_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0">
|
||||
@ -147,7 +147,7 @@ class TestFetchRss:
|
||||
assert result[0]["link"] == ""
|
||||
|
||||
@patch("utils.news._session.get")
|
||||
async def test_fetch_rss_limit_to_10(self, mock_get):
|
||||
async def test_fetch_rss_limit_to_10(self, mock_get) -> None:
|
||||
"""Больше 10 items должно быть обрезано до 10."""
|
||||
items = "\n".join(
|
||||
f""" <item>
|
||||
@ -174,7 +174,7 @@ class TestFetchRss:
|
||||
assert result[9]["title"] == "Статья 9"
|
||||
|
||||
@patch("utils.news._session.get")
|
||||
async def test_fetch_rss_http_error(self, mock_get):
|
||||
async def test_fetch_rss_http_error(self, mock_get) -> None:
|
||||
"""HTTP-ошибка должна вернуть None."""
|
||||
import requests
|
||||
mock_get.side_effect = requests.exceptions.HTTPError("404 Not Found")
|
||||
@ -182,7 +182,7 @@ class TestFetchRss:
|
||||
assert result is None
|
||||
|
||||
@patch("utils.news._session.get")
|
||||
async def test_fetch_rss_connection_error(self, mock_get):
|
||||
async def test_fetch_rss_connection_error(self, mock_get) -> None:
|
||||
"""Ошибка соединения должна вернуть None."""
|
||||
import requests
|
||||
mock_get.side_effect = requests.exceptions.ConnectionError("No connection")
|
||||
@ -190,7 +190,7 @@ class TestFetchRss:
|
||||
assert result is None
|
||||
|
||||
@patch("utils.news._session.get")
|
||||
async def test_fetch_rss_timeout(self, mock_get):
|
||||
async def test_fetch_rss_timeout(self, mock_get) -> None:
|
||||
"""Таймаут должен вернуть None."""
|
||||
import requests
|
||||
mock_get.side_effect = requests.exceptions.Timeout("Request timed out")
|
||||
@ -198,7 +198,7 @@ class TestFetchRss:
|
||||
assert result is None
|
||||
|
||||
@patch("utils.news._session.get")
|
||||
async def test_fetch_rss_ssl_error(self, mock_get):
|
||||
async def test_fetch_rss_ssl_error(self, mock_get) -> None:
|
||||
"""SSLError должен вернуть None."""
|
||||
import requests
|
||||
mock_get.side_effect = requests.exceptions.SSLError("SSL handshake failed")
|
||||
@ -206,7 +206,7 @@ class TestFetchRss:
|
||||
assert result is None
|
||||
|
||||
@patch("utils.news._session.get")
|
||||
async def test_fetch_rss_empty_tags(self, mock_get):
|
||||
async def test_fetch_rss_empty_tags(self, mock_get) -> None:
|
||||
"""Статья с пустыми тегами должна иметь пустые строки."""
|
||||
rss_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0">
|
||||
@ -231,7 +231,7 @@ class TestFetchRss:
|
||||
assert result[0]["tags"] == []
|
||||
|
||||
@patch("utils.news._session.get")
|
||||
async def test_fetch_rss_category_without_text(self, mock_get):
|
||||
async def test_fetch_rss_category_without_text(self, mock_get) -> None:
|
||||
"""Категория без текста должна быть пропущена."""
|
||||
rss_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0">
|
||||
@ -253,7 +253,7 @@ class TestFetchRss:
|
||||
assert result[0]["tags"] == ["AI"]
|
||||
|
||||
@patch("utils.news._session.get")
|
||||
async def test_fetch_rss_atom_missing_author(self, mock_get):
|
||||
async def test_fetch_rss_atom_missing_author(self, mock_get) -> None:
|
||||
"""Atom feed без автора должен иметь пустого creator."""
|
||||
atom_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
@ -273,7 +273,7 @@ class TestFetchRss:
|
||||
assert result[0]["creator"] == ""
|
||||
|
||||
@patch("utils.news._session.get")
|
||||
async def test_fetch_rss_atom_missing_link(self, mock_get):
|
||||
async def test_fetch_rss_atom_missing_link(self, mock_get) -> None:
|
||||
"""Atom feed без link должен иметь пустую ссылку."""
|
||||
atom_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
@ -292,7 +292,7 @@ class TestFetchRss:
|
||||
assert result[0]["link"] == ""
|
||||
|
||||
@patch("utils.news._session.get")
|
||||
async def test_fetch_rss_request_exception(self, mock_get):
|
||||
async def test_fetch_rss_request_exception(self, mock_get) -> None:
|
||||
"""Общий RequestException должен вернуть None."""
|
||||
import requests
|
||||
mock_get.side_effect = requests.RequestException("Generic error")
|
||||
@ -300,7 +300,7 @@ class TestFetchRss:
|
||||
assert result is None
|
||||
|
||||
@patch("utils.news._session.get")
|
||||
async def test_fetch_rss_guid_fallback_to_link(self, mock_get):
|
||||
async def test_fetch_rss_guid_fallback_to_link(self, mock_get) -> None:
|
||||
"""Если нет guid isPermaLink, ссылка должна быть пустой."""
|
||||
rss_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0">
|
||||
@ -321,7 +321,7 @@ class TestFetchRss:
|
||||
assert result[0]["link"] == ""
|
||||
|
||||
@patch("utils.news._session.get")
|
||||
async def test_fetch_rss_single_item(self, mock_get):
|
||||
async def test_fetch_rss_single_item(self, mock_get) -> None:
|
||||
"""Один item должен быть распарсен корректно."""
|
||||
rss_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0">
|
||||
@ -348,7 +348,7 @@ class TestFetchRss:
|
||||
assert result[0]["tags"] == ["ML"]
|
||||
|
||||
@patch("utils.news._session.get")
|
||||
async def test_fetch_rss_special_characters_in_title(self, mock_get):
|
||||
async def test_fetch_rss_special_characters_in_title(self, mock_get) -> None:
|
||||
"""Заголовки со спецсимволами должны парситься корректно."""
|
||||
rss_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0">
|
||||
@ -370,7 +370,7 @@ class TestFetchRss:
|
||||
assert "ML" in result[0]["title"]
|
||||
|
||||
@patch("utils.news._session.get")
|
||||
async def test_fetch_rss_date_with_gmt(self, mock_get):
|
||||
async def test_fetch_rss_date_with_gmt(self, mock_get) -> None:
|
||||
"""Дата с GMT должна парситься корректно."""
|
||||
rss_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0">
|
||||
@ -391,7 +391,7 @@ class TestFetchRss:
|
||||
assert result[0]["pub_date"] == "Mon, 28 May 2026 10:00:00 GMT"
|
||||
|
||||
@patch("utils.news._session.get")
|
||||
async def test_fetch_rss_many_categories(self, mock_get):
|
||||
async def test_fetch_rss_many_categories(self, mock_get) -> None:
|
||||
"""Множество категорий должны быть собраны."""
|
||||
rss_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0">
|
||||
|
||||
@ -8,7 +8,7 @@ class TestFetchWeather:
|
||||
"""Тесты функции fetch_weather() — получение погоды с retry-логикой."""
|
||||
|
||||
@patch("utils.pogoda._session.get")
|
||||
async def test_fetch_weather_success(self, mock_get):
|
||||
async def test_fetch_weather_success(self, mock_get) -> None:
|
||||
"""Успешный ответ должен вернуть JSON-данные."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {"current_condition": [{"temp_C": 20}]}
|
||||
@ -18,7 +18,7 @@ class TestFetchWeather:
|
||||
assert result == {"current_condition": [{"temp_C": 20}]}
|
||||
|
||||
@patch("utils.pogoda._session.get")
|
||||
async def test_fetch_weather_fallback_on_ssl_error(self, mock_get):
|
||||
async def test_fetch_weather_fallback_on_ssl_error(self, mock_get) -> None:
|
||||
"""SSLError на первой попытке → fallback на Open-Meteo."""
|
||||
from requests.exceptions import SSLError
|
||||
mock_get.side_effect = [
|
||||
@ -31,7 +31,7 @@ class TestFetchWeather:
|
||||
assert result == {"result": "fallback"}
|
||||
|
||||
@patch("utils.pogoda._session.get")
|
||||
async def test_fetch_weather_fallback_on_connection_error(self, mock_get):
|
||||
async def test_fetch_weather_fallback_on_connection_error(self, mock_get) -> None:
|
||||
"""ConnectionError → fallback на Open-Meteo."""
|
||||
from requests.exceptions import ConnectionError
|
||||
mock_get.side_effect = ConnectionError("No connection")
|
||||
@ -41,7 +41,7 @@ class TestFetchWeather:
|
||||
assert result == {"result": "fallback"}
|
||||
|
||||
@patch("utils.pogoda._session.get")
|
||||
async def test_fetch_weather_fallback_on_timeout(self, mock_get):
|
||||
async def test_fetch_weather_fallback_on_timeout(self, mock_get) -> None:
|
||||
"""Timeout → fallback на Open-Meteo."""
|
||||
from requests.exceptions import Timeout
|
||||
mock_get.side_effect = Timeout("Timed out")
|
||||
@ -51,7 +51,7 @@ class TestFetchWeather:
|
||||
assert result == {"result": "fallback"}
|
||||
|
||||
@patch("utils.pogoda._session.get")
|
||||
async def test_fetch_weather_all_retries_fail(self, mock_get):
|
||||
async def test_fetch_weather_all_retries_fail(self, mock_get) -> None:
|
||||
"""Все попытки не удались → fallback на Open-Meteo."""
|
||||
from requests.exceptions import ConnectionError
|
||||
mock_get.side_effect = ConnectionError("No connection")
|
||||
@ -61,7 +61,7 @@ class TestFetchWeather:
|
||||
assert result is None
|
||||
|
||||
@patch("utils.pogoda._session.get")
|
||||
async def test_fetch_weather_request_exception(self, mock_get):
|
||||
async def test_fetch_weather_request_exception(self, mock_get) -> None:
|
||||
"""Общий RequestException → fallback на Open-Meteo."""
|
||||
import requests
|
||||
mock_get.side_effect = requests.RequestException("Generic error")
|
||||
@ -71,7 +71,7 @@ class TestFetchWeather:
|
||||
assert result == {"result": "fallback"}
|
||||
|
||||
@patch("utils.pogoda._session.get")
|
||||
async def test_fetch_weather_http_error_no_fallback(self, mock_get):
|
||||
async def test_fetch_weather_http_error_no_fallback(self, mock_get) -> None:
|
||||
"""HTTP-ошибка (raise_for_status) не ловится, падает."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.raise_for_status.side_effect = Exception("HTTP 500")
|
||||
@ -84,7 +84,7 @@ class TestFetchOpenMeteo:
|
||||
"""Тесты функции fetch_open_meteo() — fallback на Open-Meteo API."""
|
||||
|
||||
@patch("utils.pogoda._session.get")
|
||||
async def test_fetch_open_meteo_success(self, mock_get):
|
||||
async def test_fetch_open_meteo_success(self, mock_get) -> None:
|
||||
"""Успешный ответ должен вернуть данные в формате current_condition."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {
|
||||
@ -108,7 +108,7 @@ class TestFetchOpenMeteo:
|
||||
assert result["current_condition"][0]["pressure"] == 1013
|
||||
|
||||
@patch("utils.pogoda._session.get")
|
||||
async def test_fetch_open_meteo_custom_coords(self, mock_get):
|
||||
async def test_fetch_open_meteo_custom_coords(self, mock_get) -> None:
|
||||
"""Кастомные координаты должны быть в URL."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {"current": {"temperature": 25, "apparent_temperature": 22, "weather_code": 0, "wind_speed_10m": 3, "relative_humidity_2m": 50, "pressure_msl": 1020}}
|
||||
@ -122,7 +122,7 @@ class TestFetchOpenMeteo:
|
||||
assert "37.6173" in call_url
|
||||
|
||||
@patch("utils.pogoda._session.get")
|
||||
async def test_fetch_open_meteo_missing_weather_code(self, mock_get):
|
||||
async def test_fetch_open_meteo_missing_weather_code(self, mock_get) -> None:
|
||||
"""Отсутствующий weather_code → 'Неизвестно'."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {"current": {"temperature": 10}}
|
||||
@ -133,7 +133,7 @@ class TestFetchOpenMeteo:
|
||||
assert result["current_condition"][0]["weatherDesc"] == [{"value": "Неизвестно"}]
|
||||
|
||||
@patch("utils.pogoda._session.get")
|
||||
async def test_fetch_open_meteo_ssl_error(self, mock_get):
|
||||
async def test_fetch_open_meteo_ssl_error(self, mock_get) -> None:
|
||||
"""SSLError → вернуть None."""
|
||||
from requests.exceptions import SSLError
|
||||
mock_get.side_effect = SSLError("SSL Error")
|
||||
@ -144,7 +144,7 @@ class TestFetchOpenMeteo:
|
||||
assert result is None
|
||||
|
||||
@patch("utils.pogoda._session.get")
|
||||
async def test_fetch_open_meteo_connection_error(self, mock_get):
|
||||
async def test_fetch_open_meteo_connection_error(self, mock_get) -> None:
|
||||
"""ConnectionError → вернуть None."""
|
||||
from requests.exceptions import ConnectionError
|
||||
mock_get.side_effect = ConnectionError("No connection")
|
||||
@ -152,7 +152,7 @@ class TestFetchOpenMeteo:
|
||||
assert result is None
|
||||
|
||||
@patch("utils.pogoda._session.get")
|
||||
async def test_fetch_open_meteo_timeout(self, mock_get):
|
||||
async def test_fetch_open_meteo_timeout(self, mock_get) -> None:
|
||||
"""Timeout → вернуть None."""
|
||||
from requests.exceptions import Timeout
|
||||
mock_get.side_effect = Timeout("Timed out")
|
||||
@ -160,7 +160,7 @@ class TestFetchOpenMeteo:
|
||||
assert result is None
|
||||
|
||||
@patch("utils.pogoda._session.get")
|
||||
async def test_fetch_open_meteo_request_exception(self, mock_get):
|
||||
async def test_fetch_open_meteo_request_exception(self, mock_get) -> None:
|
||||
"""Общий RequestException → вернуть None."""
|
||||
import requests
|
||||
mock_get.side_effect = requests.RequestException("Error")
|
||||
@ -168,7 +168,7 @@ class TestFetchOpenMeteo:
|
||||
assert result is None
|
||||
|
||||
@patch("utils.pogoda._session.get")
|
||||
async def test_fetch_open_meteo_json_parse_error(self, mock_get):
|
||||
async def test_fetch_open_meteo_json_parse_error(self, mock_get) -> None:
|
||||
"""Ошибка парсинга JSON → вернуть None."""
|
||||
import requests
|
||||
mock_response = MagicMock()
|
||||
@ -179,7 +179,7 @@ class TestFetchOpenMeteo:
|
||||
assert result is None
|
||||
|
||||
@patch("utils.pogoda._session.get")
|
||||
async def test_fetch_open_meteo_retry_on_error(self, mock_get):
|
||||
async def test_fetch_open_meteo_retry_on_error(self, mock_get) -> None:
|
||||
"""Retry: первая попытка падает, вторая успешна."""
|
||||
from requests.exceptions import ConnectionError
|
||||
success_response = MagicMock()
|
||||
@ -191,7 +191,7 @@ class TestFetchOpenMeteo:
|
||||
assert mock_get.call_count == 2
|
||||
|
||||
@patch("utils.pogoda._session.get")
|
||||
async def test_fetch_open_meteo_all_retries_fail(self, mock_get):
|
||||
async def test_fetch_open_meteo_all_retries_fail(self, mock_get) -> None:
|
||||
"""Все попытки неудачны → None."""
|
||||
from requests.exceptions import ConnectionError
|
||||
mock_get.side_effect = [ConnectionError("fail"), ConnectionError("fail"), ConnectionError("fail")]
|
||||
@ -200,7 +200,7 @@ class TestFetchOpenMeteo:
|
||||
assert mock_get.call_count == 3
|
||||
|
||||
@patch("utils.pogoda._session.get")
|
||||
async def test_fetch_open_meteo_http_error(self, mock_get):
|
||||
async def test_fetch_open_meteo_http_error(self, mock_get) -> None:
|
||||
"""HTTP 404 → raise_for_status бросит исключение → None."""
|
||||
import requests
|
||||
mock_response = MagicMock()
|
||||
@ -210,7 +210,7 @@ class TestFetchOpenMeteo:
|
||||
assert result is None
|
||||
|
||||
@patch("utils.pogoda._session.get")
|
||||
async def test_fetch_open_meteo_wind_speed_0(self, mock_get):
|
||||
async def test_fetch_open_meteo_wind_speed_0(self, mock_get) -> None:
|
||||
"""Нулевая скорость ветра должна корректно обрабатываться."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {
|
||||
|
||||
@ -17,11 +17,11 @@ class TestTruncateTitle:
|
||||
("A" * 50, 100, "A" * 50),
|
||||
],
|
||||
)
|
||||
def test_truncate(self, title, max_len, expected):
|
||||
def test_truncate(self, title, max_len, expected) -> None:
|
||||
"""Проверка обрезки заголовка."""
|
||||
assert truncate_title(title, max_len) == expected
|
||||
|
||||
def test_truncate_default_max_len(self):
|
||||
def test_truncate_default_max_len(self) -> None:
|
||||
"""По умолчанию max_len=60."""
|
||||
long_title = "A" * 61
|
||||
result = truncate_title(long_title)
|
||||
@ -42,7 +42,7 @@ class TestParseDate:
|
||||
("2026-01-01T00:00:00Z", "2026.01.01"),
|
||||
],
|
||||
)
|
||||
def test_parse_date_known(self, pub_date, expected):
|
||||
def test_parse_date_known(self, pub_date, expected) -> None:
|
||||
"""Известные форматы даты должны парситься корректно."""
|
||||
assert _parse_date(pub_date) == expected
|
||||
|
||||
@ -53,11 +53,11 @@ class TestParseDate:
|
||||
(None, ""),
|
||||
],
|
||||
)
|
||||
def test_parse_date_empty(self, pub_date, expected):
|
||||
def test_parse_date_empty(self, pub_date, expected) -> None:
|
||||
"""Пустая или None дата должна вернуть пустую строку."""
|
||||
assert _parse_date(pub_date) == expected
|
||||
|
||||
def test_parse_date_invalid(self):
|
||||
def test_parse_date_invalid(self) -> None:
|
||||
"""Невалидная дата должна вернуть первые 10 символов."""
|
||||
result = _parse_date("invalid-date-string")
|
||||
assert result == "invalid.da" # первые 10 символов: 'invalid-da' → 'invalid.da' (replace('-','.'))
|
||||
@ -66,7 +66,7 @@ class TestParseDate:
|
||||
class TestFormatArticles:
|
||||
"""Тесты функции format_articles() — формирование строк для вывода."""
|
||||
|
||||
def test_format_articles_normal(self):
|
||||
def test_format_articles_normal(self) -> None:
|
||||
"""Нормальный список статей должен вернуть заголовок + 5 статей."""
|
||||
articles = [
|
||||
{
|
||||
@ -90,7 +90,7 @@ class TestFormatArticles:
|
||||
assert result[1] == "Статья 1\n28.05.2026 <https://habr.com/1>"
|
||||
assert result[2] == "Статья 2\n29.05.2026 <https://habr.com/2>"
|
||||
|
||||
def test_format_articles_limit_to_5(self):
|
||||
def test_format_articles_limit_to_5(self) -> None:
|
||||
"""Больше 5 статей должно быть обрезано до 5."""
|
||||
articles = [
|
||||
{"title": f"Статья {i}", "link": f"https://habr.com/{i}", "pub_date": "Mon, 28 May 2026 10:00:00 +0000", "creator": "", "tags": []}
|
||||
@ -100,18 +100,18 @@ class TestFormatArticles:
|
||||
assert len(result) == 6 # заголовок + 5 статей
|
||||
assert result[-1] == "Статья 4\n28.05.2026 <https://habr.com/4>"
|
||||
|
||||
def test_format_articles_empty_list(self):
|
||||
def test_format_articles_empty_list(self) -> None:
|
||||
"""Пустой список должен вернуть только заголовок."""
|
||||
result = format_articles([], "Заголовок", "https://habr.com/feed")
|
||||
assert result == ["**Заголовок**\n<https://habr.com/feed>"]
|
||||
assert len(result) == 1
|
||||
|
||||
def test_format_articles_none(self):
|
||||
def test_format_articles_none(self) -> None:
|
||||
"""None должен вызвать TypeError (articles[:5] на None)."""
|
||||
with pytest.raises(TypeError):
|
||||
format_articles(None, "Заголовок", "https://habr.com/feed")
|
||||
|
||||
def test_format_articles_single_article(self):
|
||||
def test_format_articles_single_article(self) -> None:
|
||||
"""Одна статья должна быть корректно отформатирована."""
|
||||
articles = [
|
||||
{
|
||||
@ -127,7 +127,7 @@ class TestFormatArticles:
|
||||
assert result[0] == "**Новости AI**\n<https://habr.com/ai>"
|
||||
assert result[1] == "Единственная статья\n28.05.2026 <https://habr.com/1>"
|
||||
|
||||
def test_format_articles_long_title_truncated(self):
|
||||
def test_format_articles_long_title_truncated(self) -> None:
|
||||
"""Длинный заголовок должен быть обрезан до 60 символов с '...'."""
|
||||
long_title = "A" * 100
|
||||
articles = [
|
||||
@ -137,7 +137,7 @@ class TestFormatArticles:
|
||||
assert len(result[1].split("\n")[0]) == 63 # 60 + "..."
|
||||
assert result[1].split("\n")[0].endswith("...")
|
||||
|
||||
def test_format_articles_short_title_unchanged(self):
|
||||
def test_format_articles_short_title_unchanged(self) -> None:
|
||||
"""Короткий заголовок должен остаться без изменений."""
|
||||
short_title = "Кот"
|
||||
articles = [
|
||||
@ -146,7 +146,7 @@ class TestFormatArticles:
|
||||
result = format_articles(articles, "Заголовок", "https://habr.com/feed")
|
||||
assert result[1].split("\n")[0] == "Кот"
|
||||
|
||||
def test_format_articles_exact_60_chars(self):
|
||||
def test_format_articles_exact_60_chars(self) -> None:
|
||||
"""Заголовок ровно 60 символов не должен обрезаться."""
|
||||
exact_title = "A" * 60
|
||||
articles = [
|
||||
@ -156,7 +156,7 @@ class TestFormatArticles:
|
||||
assert result[1].split("\n")[0] == exact_title
|
||||
assert "..." not in result[1]
|
||||
|
||||
def test_format_articles_iso_date(self):
|
||||
def test_format_articles_iso_date(self) -> None:
|
||||
"""Дата в формате ISO должна парситься корректно."""
|
||||
articles = [
|
||||
{
|
||||
@ -170,7 +170,7 @@ class TestFormatArticles:
|
||||
result = format_articles(articles, "Заголовок", "https://habr.com/feed")
|
||||
assert result[1] == "Статья\n2026.05.28 <https://habr.com/1>"
|
||||
|
||||
def test_format_articles_empty_date(self):
|
||||
def test_format_articles_empty_date(self) -> None:
|
||||
"""Пустая дата должна быть пустой строкой."""
|
||||
articles = [
|
||||
{"title": "Статья", "link": "https://habr.com/1", "pub_date": "", "creator": "", "tags": []}
|
||||
@ -178,7 +178,7 @@ class TestFormatArticles:
|
||||
result = format_articles(articles, "Заголовок", "https://habr.com/feed")
|
||||
assert result[1] == "Статья\n <https://habr.com/1>"
|
||||
|
||||
def test_format_articles_none_date(self):
|
||||
def test_format_articles_none_date(self) -> None:
|
||||
"""None дата должна быть пустой строкой."""
|
||||
articles = [
|
||||
{"title": "Статья", "link": "https://habr.com/1", "pub_date": None, "creator": "", "tags": []}
|
||||
@ -186,7 +186,7 @@ class TestFormatArticles:
|
||||
result = format_articles(articles, "Заголовок", "https://habr.com/feed")
|
||||
assert result[1] == "Статья\n <https://habr.com/1>"
|
||||
|
||||
def test_format_articles_empty_link(self):
|
||||
def test_format_articles_empty_link(self) -> None:
|
||||
"""Пустая ссылка должна быть пустой строкой в угловых скобках."""
|
||||
articles = [
|
||||
{"title": "Статья", "link": "", "pub_date": "Mon, 28 May 2026 10:00:00 +0000", "creator": "", "tags": []}
|
||||
@ -194,7 +194,7 @@ class TestFormatArticles:
|
||||
result = format_articles(articles, "Заголовок", "https://habr.com/feed")
|
||||
assert result[1].endswith(" <>")
|
||||
|
||||
def test_format_articles_russian_title(self):
|
||||
def test_format_articles_russian_title(self) -> None:
|
||||
"""Русские заголовки должны корректно отображаться."""
|
||||
articles = [
|
||||
{
|
||||
@ -208,7 +208,7 @@ class TestFormatArticles:
|
||||
result = format_articles(articles, "Новости AI", "https://habr.com/ai")
|
||||
assert "Искусственный интеллект в медицине" in result[1]
|
||||
|
||||
def test_format_articles_exact_5_articles(self):
|
||||
def test_format_articles_exact_5_articles(self) -> None:
|
||||
"""Ровно 5 статей должно быть включено."""
|
||||
articles = [
|
||||
{"title": f"Статья {i}", "link": f"https://habr.com/{i}", "pub_date": "Mon, 28 May 2026 10:00:00 +0000", "creator": "", "tags": []}
|
||||
@ -218,7 +218,7 @@ class TestFormatArticles:
|
||||
assert len(result) == 6 # заголовок + 5 статей
|
||||
assert result[-1] == "Статья 4\n28.05.2026 <https://habr.com/4>"
|
||||
|
||||
def test_format_articles_6th_article_excluded(self):
|
||||
def test_format_articles_6th_article_excluded(self) -> None:
|
||||
"""6-я статья должна быть исключена."""
|
||||
articles = [
|
||||
{"title": f"Статья {i}", "link": f"https://habr.com/{i}", "pub_date": "Mon, 28 May 2026 10:00:00 +0000", "creator": "", "tags": []}
|
||||
|
||||
@ -15,7 +15,7 @@ class TestHelpCommandDiscord:
|
||||
cmd.__doc__ = doc
|
||||
return cmd
|
||||
|
||||
async def test_show_help_sends_simple_text(self):
|
||||
async def test_show_help_sends_simple_text(self) -> None:
|
||||
"""Проверка, что команда отправляет простое текстовое сообщение."""
|
||||
from commands.help import Help
|
||||
|
||||
@ -28,7 +28,7 @@ class TestHelpCommandDiscord:
|
||||
|
||||
mock_ctx.send.assert_awaited_once()
|
||||
|
||||
async def test_show_help_message_content(self):
|
||||
async def test_show_help_message_content(self) -> None:
|
||||
"""Проверка содержания отправленного сообщения."""
|
||||
from commands.help import Help
|
||||
|
||||
|
||||
@ -13,21 +13,21 @@ from utils.morning_runner import Scheduler, run_morning
|
||||
class TestSchedulerInit:
|
||||
"""Тесты инициализации Scheduler."""
|
||||
|
||||
def test_init_sets_morning_time(self):
|
||||
def test_init_sets_morning_time(self) -> None:
|
||||
"""Инициализация должна устанавливать время."""
|
||||
bot = AsyncMock()
|
||||
with patch.object(Scheduler, "_start_scheduler"):
|
||||
scheduler = Scheduler(bot, "08:30")
|
||||
assert scheduler.morning_time == "08:30"
|
||||
|
||||
def test_init_default_morning_time(self):
|
||||
def test_init_default_morning_time(self) -> None:
|
||||
"""Инициализация с дефолтным временем."""
|
||||
bot = AsyncMock()
|
||||
with patch.object(Scheduler, "_start_scheduler"):
|
||||
scheduler = Scheduler(bot)
|
||||
assert scheduler.morning_time == "07:00"
|
||||
|
||||
def test_init_creates_task(self):
|
||||
def test_init_creates_task(self) -> None:
|
||||
"""Инициализация должна вызывать _start_scheduler."""
|
||||
bot = AsyncMock()
|
||||
with patch.object(Scheduler, "_start_scheduler") as mock_start:
|
||||
@ -38,7 +38,7 @@ class TestSchedulerInit:
|
||||
class TestSchedulerCalculateNextRun:
|
||||
"""Тесты расчёта следующего запуска."""
|
||||
|
||||
def test_next_run_today_before_time(self):
|
||||
def test_next_run_today_before_time(self) -> None:
|
||||
"""Если сейчас раньше времени — вернуть сегодня."""
|
||||
bot = AsyncMock()
|
||||
with patch.object(Scheduler, "_start_scheduler"):
|
||||
@ -47,7 +47,7 @@ class TestSchedulerCalculateNextRun:
|
||||
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):
|
||||
def test_next_run_tomorrow_after_time(self) -> None:
|
||||
"""Если сейчас позже времени — вернуть завтра."""
|
||||
bot = AsyncMock()
|
||||
with patch.object(Scheduler, "_start_scheduler"):
|
||||
@ -56,7 +56,7 @@ class TestSchedulerCalculateNextRun:
|
||||
next_run = scheduler._calculate_next_run(now)
|
||||
assert next_run == datetime(2026, 5, 30, 14, 0, 0)
|
||||
|
||||
def test_next_run_exact_time(self):
|
||||
def test_next_run_exact_time(self) -> None:
|
||||
"""Если сейчас ровно время — вернуть завтра."""
|
||||
bot = AsyncMock()
|
||||
with patch.object(Scheduler, "_start_scheduler"):
|
||||
@ -69,7 +69,7 @@ class TestSchedulerCalculateNextRun:
|
||||
class TestSchedulerStartStop:
|
||||
"""Тесты запуска/остановки планировщика."""
|
||||
|
||||
def test_start_starts_task(self):
|
||||
def test_start_starts_task(self) -> None:
|
||||
"""start() должен вызывать _start_scheduler (1 в __init__ + 1 в start, но реальный task один)."""
|
||||
bot = AsyncMock()
|
||||
with patch.object(Scheduler, "_start_scheduler") as mock_start:
|
||||
@ -78,7 +78,7 @@ class TestSchedulerStartStop:
|
||||
# __init__ вызывает _start_scheduler, start() тоже вызывает
|
||||
assert mock_start.call_count == 2
|
||||
|
||||
def test_stop_stops_task(self):
|
||||
def test_stop_stops_task(self) -> None:
|
||||
"""stop() должен остановить task."""
|
||||
bot = AsyncMock()
|
||||
with patch("asyncio.create_task"):
|
||||
@ -86,7 +86,7 @@ class TestSchedulerStartStop:
|
||||
scheduler.stop()
|
||||
assert scheduler._running is False
|
||||
|
||||
def test_double_start_no_duplicate(self):
|
||||
def test_double_start_no_duplicate(self) -> None:
|
||||
"""Повторный start должен вызывать _start_scheduler дважды (реальный task не дублируется благодаря флагам)."""
|
||||
bot = AsyncMock()
|
||||
with patch.object(Scheduler, "_start_scheduler") as mock_start:
|
||||
@ -99,7 +99,7 @@ class TestRunMorning:
|
||||
"""Тесты run_morning."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_morning_sends_embed(self):
|
||||
async def test_run_morning_sends_embed(self) -> None:
|
||||
"""run_morning должен отправлять embed в канал."""
|
||||
bot = AsyncMock()
|
||||
channel = AsyncMock()
|
||||
@ -131,7 +131,7 @@ class TestRunMorningWithFallback:
|
||||
"""Тесты fallback в пустом embed."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_morning_empty_embed_fallback(self):
|
||||
async def test_run_morning_empty_embed_fallback(self) -> None:
|
||||
"""run_morning должен добавлять fallback сообщение при пустых данных."""
|
||||
bot = AsyncMock()
|
||||
channel = AsyncMock()
|
||||
@ -160,7 +160,7 @@ class TestRunMorningWithFallback:
|
||||
assert "Не удалось получить данные из внешних источников" in embed_description
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_morning_only_weather_data(self):
|
||||
async def test_run_morning_only_weather_data(self) -> None:
|
||||
"""run_morning должен корректно обрабатывать только погоду без новостей."""
|
||||
bot = AsyncMock()
|
||||
channel = AsyncMock()
|
||||
|
||||
@ -5,7 +5,7 @@ from utils.pogoda import translate_weather, pressure_to_mmhg, wmo_to_russian, fo
|
||||
class TestFormatWeatherDataForConsole:
|
||||
"""Тесты функции format_weather_data_for_console()."""
|
||||
|
||||
def test_format_valid_data(self):
|
||||
def test_format_valid_data(self) -> None:
|
||||
"""Полные данные должны быть отформатированы корректно."""
|
||||
data = {
|
||||
"current_condition": [{
|
||||
@ -28,7 +28,7 @@ class TestFormatWeatherDataForConsole:
|
||||
assert "Ветер: 2.8 м/с" in result[3] # 10 / 3.6 = 2.777... ≈ 2.8
|
||||
assert "Давление: 759.8 мм рт. ст." in result[4]
|
||||
|
||||
def test_format_empty_data(self):
|
||||
def test_format_empty_data(self) -> None:
|
||||
"""Пустые данные должны возвращать None."""
|
||||
data = {
|
||||
"current_condition": [{}]
|
||||
@ -38,7 +38,7 @@ class TestFormatWeatherDataForConsole:
|
||||
|
||||
assert result is None, "Пустые данные должны возвращать None"
|
||||
|
||||
def test_format_missing_current_condition(self):
|
||||
def test_format_missing_current_condition(self) -> None:
|
||||
"""Отсутствие current_condition должно вернуть None."""
|
||||
data = {}
|
||||
|
||||
@ -46,7 +46,7 @@ class TestFormatWeatherDataForConsole:
|
||||
|
||||
assert result is None, "Отсутствие current_condition должно вернуть None"
|
||||
|
||||
def test_format_with_dashes(self):
|
||||
def test_format_with_dashes(self) -> None:
|
||||
"""Неизвестные значения должны отображаться как '—'."""
|
||||
data = {
|
||||
"current_condition": [{
|
||||
@ -68,7 +68,7 @@ class TestFormatWeatherDataForConsole:
|
||||
assert "Ветер: — м/с" in result[3]
|
||||
assert "Давление: — мм рт. ст." in result[4]
|
||||
|
||||
def test_format_wind_conversion(self):
|
||||
def test_format_wind_conversion(self) -> None:
|
||||
"""Проверка конвертации ветра из км/ч в м/с."""
|
||||
data = {
|
||||
"current_condition": [{
|
||||
@ -85,7 +85,7 @@ class TestFormatWeatherDataForConsole:
|
||||
# 36 / 3.6 = 10.0
|
||||
assert "Ветер: 10.0 м/с" in result[3]
|
||||
|
||||
def test_format_negative_temperature(self):
|
||||
def test_format_negative_temperature(self) -> None:
|
||||
"""Отрицательная температура должна отображаться корректно."""
|
||||
data = {
|
||||
"current_condition": [{
|
||||
@ -141,7 +141,7 @@ class TestTranslateWeather:
|
||||
("Moderate or heavy rain in area", "Дождь"),
|
||||
],
|
||||
)
|
||||
def test_translate_known(self, english, expected):
|
||||
def test_translate_known(self, english, expected) -> None:
|
||||
"""Известные переводы должны возвращать ожидаемый результат."""
|
||||
assert translate_weather(english) == expected
|
||||
|
||||
@ -153,34 +153,34 @@ class TestTranslateWeather:
|
||||
(" ", " "), # пробелы не считаются пустыми
|
||||
],
|
||||
)
|
||||
def test_translate_empty(self, input_value, expected):
|
||||
def test_translate_empty(self, input_value, expected) -> None:
|
||||
"""Пустой или None ввод должен возвращать '—'."""
|
||||
assert translate_weather(input_value) == expected
|
||||
|
||||
def test_translate_unknown_returns_original(self):
|
||||
def test_translate_unknown_returns_original(self) -> None:
|
||||
"""Неизвестный перевод должен возвращать оригинальный текст."""
|
||||
unknown_text = "Unknown weather condition XYZ"
|
||||
assert translate_weather(unknown_text) == unknown_text
|
||||
|
||||
def test_translate_partial_match(self):
|
||||
def test_translate_partial_match(self) -> None:
|
||||
"""Частичное совпадение ключа в тексте должно сработать."""
|
||||
# "Moderate or heavy rain in area" должно найтись в "Light Moderate or heavy rain in area"
|
||||
text_with_prefix = "Light Moderate or heavy rain in area"
|
||||
assert translate_weather(text_with_prefix) == "Дождь"
|
||||
|
||||
def test_translate_longer_key_priority(self):
|
||||
def test_translate_longer_key_priority(self) -> None:
|
||||
"""Длинные ключи проверяются первыми (_WEATHER_MAPPING отсортирован по убыванию длины).
|
||||
"Moderate or heavy rain at times" проверится до "Heavy rain"."""
|
||||
text = "Moderate or heavy rain at times"
|
||||
assert translate_weather(text) == "Дождь"
|
||||
|
||||
def test_translate_case_insensitive(self):
|
||||
def test_translate_case_insensitive(self) -> None:
|
||||
"""Перевод должен быть регистронезависимым."""
|
||||
assert translate_weather("CLEAR") == "Ясно"
|
||||
assert translate_weather("partly cloudy") == "Переменная облачность"
|
||||
assert translate_weather("HEAVY RAIN") == "Сильный дождь"
|
||||
|
||||
def test_translate_with_whitespace(self):
|
||||
def test_translate_with_whitespace(self) -> None:
|
||||
"""Текст с пробелами по краям должен корректно переводиться."""
|
||||
assert translate_weather(" Clear ") == "Ясно"
|
||||
|
||||
@ -198,7 +198,7 @@ class TestPressureToMMHG:
|
||||
# (0, "—"), # 0 — falsy, возвращается '—' (баг)
|
||||
],
|
||||
)
|
||||
def test_pressure_valid(self, mb, expected):
|
||||
def test_pressure_valid(self, mb, expected) -> None:
|
||||
"""Валидные числовые значения должны конвертироваться корректно."""
|
||||
assert pressure_to_mmhg(mb) == expected
|
||||
|
||||
@ -210,7 +210,7 @@ class TestPressureToMMHG:
|
||||
("980", 735.1),
|
||||
],
|
||||
)
|
||||
def test_pressure_string(self, mb, expected):
|
||||
def test_pressure_string(self, mb, expected) -> None:
|
||||
"""Строка-число должна конвертироваться корректно."""
|
||||
assert pressure_to_mmhg(mb) == expected
|
||||
|
||||
@ -222,27 +222,27 @@ class TestPressureToMMHG:
|
||||
("", "—"),
|
||||
],
|
||||
)
|
||||
def test_pressure_invalid(self, input_value, expected):
|
||||
def test_pressure_invalid(self, input_value, expected) -> None:
|
||||
"""Невалидные значения должны возвращать '—'."""
|
||||
assert pressure_to_mmhg(input_value) == expected
|
||||
|
||||
def test_pressure_non_numeric_string(self):
|
||||
def test_pressure_non_numeric_string(self) -> None:
|
||||
"""Невалидная строка должна возвращать '—'."""
|
||||
assert pressure_to_mmhg("abc") == "—"
|
||||
|
||||
def test_pressure_zero(self):
|
||||
def test_pressure_zero(self) -> None:
|
||||
"""Нулевое значение — корректно конвертируется в 0.0."""
|
||||
assert pressure_to_mmhg(0) == 0.0
|
||||
|
||||
def test_pressure_negative(self):
|
||||
def test_pressure_negative(self) -> None:
|
||||
"""Отрицательное значение должно конвертироваться."""
|
||||
assert pressure_to_mmhg(-100) == -75.0
|
||||
|
||||
def test_pressure_float_string(self):
|
||||
def test_pressure_float_string(self) -> None:
|
||||
"""Строка с десятичной точкой должна конвертироваться."""
|
||||
assert pressure_to_mmhg("1013.25") == 760.0
|
||||
|
||||
def test_pressure_very_large(self):
|
||||
def test_pressure_very_large(self) -> None:
|
||||
"""Очень большое значение должно работать."""
|
||||
assert pressure_to_mmhg(999999) == 750061.2
|
||||
|
||||
@ -283,26 +283,26 @@ class TestWmoToRussian:
|
||||
(99, "Сильная гроза с градом"),
|
||||
],
|
||||
)
|
||||
def test_wmo_known(self, code, expected):
|
||||
def test_wmo_known(self, code, expected) -> None:
|
||||
"""Известные WMO коды должны возвращать ожидаемый перевод."""
|
||||
assert wmo_to_russian(code) == expected
|
||||
|
||||
def test_wmo_unknown(self):
|
||||
def test_wmo_unknown(self) -> None:
|
||||
"""Неизвестный код должен возвращать 'Неизвестно'."""
|
||||
assert wmo_to_russian(999) == "Неизвестно"
|
||||
|
||||
def test_wmo_negative_code(self):
|
||||
def test_wmo_negative_code(self) -> None:
|
||||
"""Отрицательный код должен возвращать 'Неизвестно'."""
|
||||
assert wmo_to_russian(-1) == "Неизвестно"
|
||||
|
||||
def test_wmo_none(self):
|
||||
def test_wmo_none(self) -> None:
|
||||
"""None должен возвращать 'Неизвестно'."""
|
||||
assert wmo_to_russian(None) == "Неизвестно"
|
||||
|
||||
def test_wmo_large_code(self):
|
||||
def test_wmo_large_code(self) -> None:
|
||||
"""Очень большой код должен возвращать 'Неизвестно'."""
|
||||
assert wmo_to_russian(9999) == "Неизвестно"
|
||||
|
||||
def test_wmo_float_code(self):
|
||||
def test_wmo_float_code(self) -> None:
|
||||
"""Дробный код — не найдётся в mapping."""
|
||||
assert wmo_to_russian(1.5) == "Неизвестно"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user