diff --git a/tests/test_bot.py b/tests/test_bot.py index 8905bf7..76e79f9 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -11,6 +11,8 @@ import sys from pathlib import Path from unittest.mock import MagicMock, patch +import discord + # Добавляем корень проекта в путь импорта ROOT_DIR = Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT_DIR)) @@ -38,7 +40,6 @@ class TestBotErrorHandling: def test_bot_handles_login_failure(self) -> None: """BotRunner.run() обрабатывает discord.LoginFailure.""" import bot - import discord runner = bot.BotRunner() with patch.object(runner.bot, "start", side_effect=discord.LoginFailure("bad token")): @@ -51,7 +52,6 @@ class TestBotErrorHandling: def test_bot_handles_http_exception(self) -> None: """BotRunner.run() обрабатывает discord.HTTPException.""" import bot - import discord runner = bot.BotRunner() mock_response = MagicMock(status=502) diff --git a/tests/test_commands_stats.py b/tests/test_commands_stats.py index 7f75f7a..f96f9df 100644 --- a/tests/test_commands_stats.py +++ b/tests/test_commands_stats.py @@ -2,6 +2,7 @@ import pytest from unittest.mock import AsyncMock, MagicMock +from commands.stats import Stats class TestStatsCommand: """Тесты Discord-команды stats.""" @@ -15,8 +16,6 @@ class TestStatsCommand: async def test_stats_sends_embed(self) -> None: """Команда stats отправляет embed-сообщение.""" - from commands.stats import Stats - mock_ctx = MagicMock() mock_ctx.bot.latency = 0.035 mock_ctx.bot.guilds = [] @@ -32,8 +31,6 @@ class TestStatsCommand: async def test_stats_correct_values(self) -> None: """Значения серверов, каналов и пользователей считаются верно.""" - from commands.stats import Stats - guild1 = self._make_mock_guild(channel_count=10, member_count=200) guild2 = self._make_mock_guild(channel_count=5, member_count=50) @@ -56,8 +53,6 @@ class TestStatsCommand: async def test_stats_empty_guilds(self) -> None: """Пустой список серверов не вызывает ошибок.""" - from commands.stats import Stats - mock_ctx = MagicMock() mock_ctx.bot.latency = 0.050 mock_ctx.bot.guilds = [] @@ -75,8 +70,6 @@ class TestStatsCommand: async def test_stats_none_member_count(self) -> None: """member_count=None не вызывает ошибок.""" - from commands.stats import Stats - guild = self._make_mock_guild(channel_count=3, member_count=None) mock_ctx = MagicMock() @@ -95,7 +88,6 @@ class TestStatsCommand: async def test_stats_excludes_categories(self) -> None: """Категории не входят в счётчик каналов.""" import discord - from commands.stats import Stats guild = MagicMock() text_ch = MagicMock() diff --git a/tests/test_commands_status.py b/tests/test_commands_status.py index d70b025..0311687 100644 --- a/tests/test_commands_status.py +++ b/tests/test_commands_status.py @@ -4,14 +4,14 @@ import time import pytest from unittest.mock import AsyncMock, MagicMock, patch +from commands.status import Status + class TestStatusCommand: """Тесты Discord-команды status.""" async def test_status_sends_embed(self) -> None: """Команда status отправляет embed-сообщение.""" - from commands.status import Status - mock_ctx = MagicMock() mock_ctx.bot.latency = 0.042 mock_ctx.bot._start_time = time.time() @@ -28,8 +28,6 @@ class TestStatusCommand: async def test_status_uptime_format(self) -> None: """Uptime форматируется корректно.""" - from commands.status import Status - mock_ctx = MagicMock() mock_ctx.bot.latency = 0.050 mock_ctx.bot._start_time = time.time() - 90061 # 1д 1ч 1м 1с @@ -51,29 +49,21 @@ class TestFormatUptime: """Тесты форматирования uptime.""" 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) -> 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) -> None: - from commands.status import Status - result = Status._format_uptime(3661) # 1ч 1м 1с assert "1ч" in result assert "1м" in result assert "1с" in result def test_full_day(self) -> None: - from commands.status import Status - result = Status._format_uptime(90061) # 1д 1ч 1м 1с assert "1д" in result assert "1ч" in result diff --git a/tests/test_fetch_cat.py b/tests/test_fetch_cat.py index c00cb3a..be2e7a3 100644 --- a/tests/test_fetch_cat.py +++ b/tests/test_fetch_cat.py @@ -1,5 +1,7 @@ import json import pytest +import requests +from requests.exceptions import ConnectionError, Timeout, SSLError from unittest.mock import patch, MagicMock from utils.cat import fetch_cat @@ -30,7 +32,6 @@ class TestFetchCat: @patch("utils.cat._session.get") async def test_fetch_cat_http_error(self, mock_get) -> None: """HTTP-ошибка (raise_for_status) должна вернуть None.""" - import requests mock_response = MagicMock() mock_response.raise_for_status.side_effect = requests.HTTPError("404 Not Found") mock_get.return_value = mock_response @@ -40,7 +41,6 @@ class TestFetchCat: @patch("utils.cat._session.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") result = await fetch_cat() assert result is None @@ -48,7 +48,6 @@ class TestFetchCat: @patch("utils.cat._session.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") result = await fetch_cat() assert result is None @@ -56,7 +55,6 @@ class TestFetchCat: @patch("utils.cat._session.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") result = await fetch_cat() assert result is None @@ -64,7 +62,6 @@ class TestFetchCat: @patch("utils.cat._session.get") async def test_fetch_cat_json_parse_error(self, mock_get) -> None: """Ошибка парсинга JSON должна вернуть None.""" - import requests mock_response = MagicMock() mock_response.json.side_effect = requests.JSONDecodeError("Expecting value", "", 0) mock_response.raise_for_status = MagicMock() @@ -85,7 +82,6 @@ class TestFetchCat: @patch("utils.cat._session.get") async def test_fetch_cat_request_exception(self, mock_get) -> None: """Общий RequestException должен вернуть None.""" - import requests mock_get.side_effect = requests.RequestException("Generic error") result = await fetch_cat() assert result is None diff --git a/tests/test_fetch_rss.py b/tests/test_fetch_rss.py index 5bc4770..dfd5f79 100644 --- a/tests/test_fetch_rss.py +++ b/tests/test_fetch_rss.py @@ -1,5 +1,6 @@ import pytest +import requests from unittest.mock import patch, MagicMock from utils.news import fetch_rss @@ -176,7 +177,6 @@ class TestFetchRss: @patch("utils.news._session.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") result = await fetch_rss("https://example.com/rss") assert result is None @@ -184,7 +184,6 @@ class TestFetchRss: @patch("utils.news._session.get") async def test_fetch_rss_connection_error(self, mock_get) -> None: """Ошибка соединения должна вернуть None.""" - import requests mock_get.side_effect = requests.exceptions.ConnectionError("No connection") result = await fetch_rss("https://example.com/rss") assert result is None @@ -192,7 +191,6 @@ class TestFetchRss: @patch("utils.news._session.get") async def test_fetch_rss_timeout(self, mock_get) -> None: """Таймаут должен вернуть None.""" - import requests mock_get.side_effect = requests.exceptions.Timeout("Request timed out") result = await fetch_rss("https://example.com/rss") assert result is None @@ -200,7 +198,6 @@ class TestFetchRss: @patch("utils.news._session.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") result = await fetch_rss("https://example.com/rss") assert result is None @@ -294,7 +291,6 @@ class TestFetchRss: @patch("utils.news._session.get") async def test_fetch_rss_request_exception(self, mock_get) -> None: """Общий RequestException должен вернуть None.""" - import requests mock_get.side_effect = requests.RequestException("Generic error") result = await fetch_rss("https://example.com/rss") assert result is None diff --git a/tests/test_fetch_weather.py b/tests/test_fetch_weather.py index 1495d20..407b14f 100644 --- a/tests/test_fetch_weather.py +++ b/tests/test_fetch_weather.py @@ -1,5 +1,7 @@ import pytest +import requests +from requests.exceptions import ConnectionError, Timeout, SSLError from unittest.mock import patch, MagicMock from utils.pogoda import fetch_weather, fetch_open_meteo @@ -20,7 +22,6 @@ class TestFetchWeather: @patch("utils.pogoda._session.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 = [ SSLError("SSL Error"), MagicMock(json=MagicMock(return_value={"result": "fallback"})), @@ -33,7 +34,6 @@ class TestFetchWeather: @patch("utils.pogoda._session.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") with patch("utils.pogoda.fetch_open_meteo") as mock_fallback: mock_fallback.return_value = {"result": "fallback"} @@ -43,7 +43,6 @@ class TestFetchWeather: @patch("utils.pogoda._session.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") with patch("utils.pogoda.fetch_open_meteo") as mock_fallback: mock_fallback.return_value = {"result": "fallback"} @@ -53,7 +52,6 @@ class TestFetchWeather: @patch("utils.pogoda._session.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") with patch("utils.pogoda.fetch_open_meteo") as mock_fallback: mock_fallback.return_value = None @@ -63,7 +61,6 @@ class TestFetchWeather: @patch("utils.pogoda._session.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") with patch("utils.pogoda.fetch_open_meteo") as mock_fallback: mock_fallback.return_value = {"result": "fallback"} @@ -135,7 +132,6 @@ class TestFetchOpenMeteo: @patch("utils.pogoda._session.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") with patch("utils.pogoda.fetch_open_meteo") as mock_fallback: # Внутренний fallback тоже падает, проверяем что возвращается None @@ -146,7 +142,6 @@ class TestFetchOpenMeteo: @patch("utils.pogoda._session.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") result = await fetch_open_meteo() assert result is None @@ -154,7 +149,6 @@ class TestFetchOpenMeteo: @patch("utils.pogoda._session.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") result = await fetch_open_meteo() assert result is None @@ -162,7 +156,6 @@ class TestFetchOpenMeteo: @patch("utils.pogoda._session.get") async def test_fetch_open_meteo_request_exception(self, mock_get) -> None: """Общий RequestException → вернуть None.""" - import requests mock_get.side_effect = requests.RequestException("Error") result = await fetch_open_meteo() assert result is None @@ -170,7 +163,6 @@ class TestFetchOpenMeteo: @patch("utils.pogoda._session.get") async def test_fetch_open_meteo_json_parse_error(self, mock_get) -> None: """Ошибка парсинга JSON → вернуть None.""" - import requests mock_response = MagicMock() mock_response.json.side_effect = requests.RequestException("JSON Error") mock_response.raise_for_status = MagicMock() @@ -181,7 +173,6 @@ class TestFetchOpenMeteo: @patch("utils.pogoda._session.get") async def test_fetch_open_meteo_retry_on_error(self, mock_get) -> None: """Retry: первая попытка падает, вторая успешна.""" - from requests.exceptions import ConnectionError success_response = MagicMock() success_response.json.return_value = {"current": {"temperature": 20, "apparent_temperature": 18, "weather_code": 1, "wind_speed_10m": 4, "relative_humidity_2m": 60, "pressure_msl": 1015}} success_response.raise_for_status = MagicMock() @@ -193,7 +184,6 @@ class TestFetchOpenMeteo: @patch("utils.pogoda._session.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")] result = await fetch_open_meteo(max_retries=3) assert result is None @@ -202,7 +192,6 @@ class TestFetchOpenMeteo: @patch("utils.pogoda._session.get") async def test_fetch_open_meteo_http_error(self, mock_get) -> None: """HTTP 404 → raise_for_status бросит исключение → None.""" - import requests mock_response = MagicMock() mock_response.raise_for_status.side_effect = requests.HTTPError("HTTP 404") mock_get.return_value = mock_response diff --git a/tests/test_help_discord.py b/tests/test_help_discord.py index 458ce93..2647f78 100644 --- a/tests/test_help_discord.py +++ b/tests/test_help_discord.py @@ -3,6 +3,8 @@ import pytest from unittest.mock import AsyncMock, MagicMock +from commands.help import Help + class TestHelpCommandDiscord: """Тесты для команды help на Discord.""" @@ -17,8 +19,6 @@ class TestHelpCommandDiscord: async def test_show_help_sends_simple_text(self) -> None: """Проверка, что команда отправляет простое текстовое сообщение.""" - from commands.help import Help - mock_ctx = MagicMock() mock_ctx.bot.commands = [] mock_ctx.send = AsyncMock(return_value=None) @@ -30,8 +30,6 @@ class TestHelpCommandDiscord: async def test_show_help_message_content(self) -> None: """Проверка содержания отправленного сообщения.""" - from commands.help import Help - message_calls = [] def send_side_effect(text: str, *args, **kwargs): diff --git a/tests/test_logger.py b/tests/test_logger.py index 7cd829e..da979ec 100644 --- a/tests/test_logger.py +++ b/tests/test_logger.py @@ -1,6 +1,8 @@ """Тесты для utils/logger.py — проверка настройки логирования.""" +import io import logging +import logging.handlers import os import tempfile from pathlib import Path @@ -43,8 +45,6 @@ def test_invalid_level_defaults_to_info() -> None: def test_file_handler_when_logs_dir_exists() -> None: """RotatingFileHandler добавляется если директория logs существует.""" - import logging.handlers - orig_cwd = os.getcwd() with tempfile.TemporaryDirectory() as tmpdir: logs_dir = Path(tmpdir) / "logs" @@ -68,8 +68,6 @@ def test_file_handler_when_logs_dir_exists() -> None: def test_logs_dir_created_automatically() -> None: """Директория logs создаётся автоматически, если её нет.""" - import logging.handlers - orig_cwd = os.getcwd() with tempfile.TemporaryDirectory() as tmpdir: os.chdir(tmpdir) @@ -109,9 +107,6 @@ def test_discord_level_is_info() -> None: def test_log_message_format() -> None: """Формат сообщения: время, уровень, имя модуля, текст.""" - import io - import sys - with _isolated_logger() as root: # Replace stdout with our buffer buffer = io.StringIO()