diff --git a/tests/test_bot.py b/tests/test_bot.py index cdc031c..8905bf7 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -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() diff --git a/tests/test_commands_pg.py b/tests/test_commands_pg.py index ff5edbe..1e813fc 100644 --- a/tests/test_commands_pg.py +++ b/tests/test_commands_pg.py @@ -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() diff --git a/tests/test_commands_stats.py b/tests/test_commands_stats.py index ca5dd92..7f75f7a 100644 --- a/tests/test_commands_stats.py +++ b/tests/test_commands_stats.py @@ -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 diff --git a/tests/test_commands_status.py b/tests/test_commands_status.py index f214c1f..d70b025 100644 --- a/tests/test_commands_status.py +++ b/tests/test_commands_status.py @@ -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с diff --git a/tests/test_fetch_cat.py b/tests/test_fetch_cat.py index bceb89c..c00cb3a 100644 --- a/tests/test_fetch_cat.py +++ b/tests/test_fetch_cat.py @@ -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"}] diff --git a/tests/test_fetch_rss.py b/tests/test_fetch_rss.py index 25e98a7..5bc4770 100644 --- a/tests/test_fetch_rss.py +++ b/tests/test_fetch_rss.py @@ -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 = """ @@ -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 = """ @@ -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 = """ @@ -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 = """ """.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 = """ @@ -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 = """ @@ -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""" @@ -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 = """ @@ -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 = """ @@ -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 = """ @@ -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 = """ @@ -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 = """ @@ -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 = """ @@ -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 = """ @@ -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 = """ @@ -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 = """ diff --git a/tests/test_fetch_weather.py b/tests/test_fetch_weather.py index 956b1bd..1495d20 100644 --- a/tests/test_fetch_weather.py +++ b/tests/test_fetch_weather.py @@ -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 = { diff --git a/tests/test_format_articles.py b/tests/test_format_articles.py index e576c39..4602516 100644 --- a/tests/test_format_articles.py +++ b/tests/test_format_articles.py @@ -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 " assert result[2] == "Статья 2\n29.05.2026 " - 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 " - 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"] 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" assert result[1] == "Единственная статья\n28.05.2026 " - 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 " - 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 " - 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 " - 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 " - 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": []} diff --git a/tests/test_help_discord.py b/tests/test_help_discord.py index 8dadf53..458ce93 100644 --- a/tests/test_help_discord.py +++ b/tests/test_help_discord.py @@ -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 diff --git a/tests/test_morning_runner.py b/tests/test_morning_runner.py index 55733e6..3210126 100644 --- a/tests/test_morning_runner.py +++ b/tests/test_morning_runner.py @@ -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() diff --git a/tests/test_pogoda.py b/tests/test_pogoda.py index 6c40970..7a1b146 100644 --- a/tests/test_pogoda.py +++ b/tests/test_pogoda.py @@ -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) == "Неизвестно"