diff --git a/CODE_REVIEW.md b/CODE_REVIEW.md index 7df0c27..bf3eb88 100644 --- a/CODE_REVIEW.md +++ b/CODE_REVIEW.md @@ -194,7 +194,7 @@ async def on_ready() -> None: --- -### H5. `asyncio.run()` в 50+ тестах вместо `async def` +### H5. `asyncio.run()` в 50+ тестах вместо `async def` — **ИСПРАВЛЕНО** **Файлы:** - `tests/test_fetch_cat.py` -- 10 вхождений @@ -540,7 +540,7 @@ DeprecationWarning: 'asyncio.iscoroutinefunction' is deprecated and slated for r 4. [x] H2: Отсортировать ключи `translate_weather` по убыванию длины — **выполнено** 5. [x] H3: Исправить `pressure_to_mmhg` -- `not mb` -> `mb is None or mb == ""` — **выполнено** 6. [x] H4: Добавить защиту от дублирования cogs в `on_ready` — **выполнено** -7. [ ] H5: Заменить `asyncio.run()` на `async def` в fetch-тестах (50+ тестов) +7. [x] H5: Заменить `asyncio.run()` на `async def` в fetch-тестах (50+ тестов) — **выполнено** 8. [ ] H6: Добавить HTTPError в retry-блок `fetch_weather` 9. [ ] H7: Добавить HTTPException/BadRequest в `on_command_error` 10. [ ] H8: Исправить утечку корутины в тестах Scheduler diff --git a/tests/test_fetch_cat.py b/tests/test_fetch_cat.py index 7db355d..bceb89c 100644 --- a/tests/test_fetch_cat.py +++ b/tests/test_fetch_cat.py @@ -1,4 +1,3 @@ -import asyncio import json import pytest from unittest.mock import patch, MagicMock @@ -9,94 +8,94 @@ class TestFetchCat: """Тесты функции fetch_cat() — получение URL случайного котика.""" @patch("utils.cat._session.get") - def test_fetch_cat_success(self, mock_get): + async def test_fetch_cat_success(self, mock_get): """Успешный ответ с URL должен вернуть строку.""" mock_response = MagicMock() mock_response.json.return_value = [{"url": "https://example.com/cat.jpg"}] mock_response.raise_for_status = MagicMock() mock_get.return_value = mock_response - result = asyncio.run(fetch_cat()) + result = await fetch_cat() assert result == "https://example.com/cat.jpg" @patch("utils.cat._session.get") - def test_fetch_cat_empty_array(self, mock_get): + async def test_fetch_cat_empty_array(self, mock_get): """Пустой массив должен вернуть None.""" mock_response = MagicMock() mock_response.json.return_value = [] mock_response.raise_for_status = MagicMock() mock_get.return_value = mock_response - result = asyncio.run(fetch_cat()) + result = await fetch_cat() assert result is None @patch("utils.cat._session.get") - def test_fetch_cat_http_error(self, mock_get): + async def test_fetch_cat_http_error(self, mock_get): """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 - result = asyncio.run(fetch_cat()) + result = await fetch_cat() assert result is None @patch("utils.cat._session.get") - def test_fetch_cat_connection_error(self, mock_get): + async def test_fetch_cat_connection_error(self, mock_get): """ConnectionError должна вернуть None.""" from requests.exceptions import ConnectionError mock_get.side_effect = ConnectionError("No connection") - result = asyncio.run(fetch_cat()) + result = await fetch_cat() assert result is None @patch("utils.cat._session.get") - def test_fetch_cat_timeout(self, mock_get): + async def test_fetch_cat_timeout(self, mock_get): """Timeout должна вернуть None.""" from requests.exceptions import Timeout mock_get.side_effect = Timeout("Request timed out") - result = asyncio.run(fetch_cat()) + result = await fetch_cat() assert result is None @patch("utils.cat._session.get") - def test_fetch_cat_ssl_error(self, mock_get): + async def test_fetch_cat_ssl_error(self, mock_get): """SSLError должна вернуть None.""" from requests.exceptions import SSLError mock_get.side_effect = SSLError("SSL handshake failed") - result = asyncio.run(fetch_cat()) + result = await fetch_cat() assert result is None @patch("utils.cat._session.get") - def test_fetch_cat_json_parse_error(self, mock_get): + async def test_fetch_cat_json_parse_error(self, mock_get): """Ошибка парсинга JSON должна вернуть None.""" import requests mock_response = MagicMock() mock_response.json.side_effect = requests.JSONDecodeError("Expecting value", "", 0) mock_response.raise_for_status = MagicMock() mock_get.return_value = mock_response - result = asyncio.run(fetch_cat()) + result = await fetch_cat() assert result is None @patch("utils.cat._session.get") - def test_fetch_cat_missing_url_key(self, mock_get): + async def test_fetch_cat_missing_url_key(self, mock_get): """Отсутствие ключа 'url' в ответе должно вернуть None.""" mock_response = MagicMock() mock_response.json.return_value = [{"error": "no image"}] mock_response.raise_for_status = MagicMock() mock_get.return_value = mock_response - result = asyncio.run(fetch_cat()) + result = await fetch_cat() assert result is None @patch("utils.cat._session.get") - def test_fetch_cat_request_exception(self, mock_get): + async def test_fetch_cat_request_exception(self, mock_get): """Общий RequestException должен вернуть None.""" import requests mock_get.side_effect = requests.RequestException("Generic error") - result = asyncio.run(fetch_cat()) + result = await fetch_cat() assert result is None @patch("utils.cat._session.get") - def test_fetch_cat_url_with_special_chars(self, mock_get): + async def test_fetch_cat_url_with_special_chars(self, mock_get): """URL со спецсимволами должен вернуться как есть.""" mock_response = MagicMock() mock_response.json.return_value = [{"url": "https://example.com/cat?w=100&h=200"}] mock_response.raise_for_status = MagicMock() mock_get.return_value = mock_response - result = asyncio.run(fetch_cat()) + result = await fetch_cat() assert result == "https://example.com/cat?w=100&h=200" diff --git a/tests/test_fetch_rss.py b/tests/test_fetch_rss.py index 99cce58..25e98a7 100644 --- a/tests/test_fetch_rss.py +++ b/tests/test_fetch_rss.py @@ -1,4 +1,4 @@ -import asyncio + import pytest from unittest.mock import patch, MagicMock from utils.news import fetch_rss @@ -8,7 +8,7 @@ class TestFetchRss: """Тесты функции fetch_rss() — получение и парсинг RSS-ленты.""" @patch("utils.news._session.get") - def test_fetch_rss_success_rss20(self, mock_get): + async def test_fetch_rss_success_rss20(self, mock_get): """Успешный ответ RSS 2.0 должен вернуть список статей.""" rss_content = """ @@ -34,7 +34,7 @@ class TestFetchRss: mock_response.content = rss_content mock_response.raise_for_status = MagicMock() mock_get.return_value = mock_response - result = asyncio.run(fetch_rss("https://example.com/rss")) + result = await fetch_rss("https://example.com/rss") assert result is not None assert len(result) == 2 assert result[0]["title"] == "Статья 1" @@ -47,7 +47,7 @@ class TestFetchRss: assert result[1]["tags"] == [] @patch("utils.news._session.get") - def test_fetch_rss_success_atom(self, mock_get): + async def test_fetch_rss_success_atom(self, mock_get): """Успешный ответ Atom должен вернуть список статей.""" atom_content = """ @@ -63,7 +63,7 @@ class TestFetchRss: mock_response.content = atom_content mock_response.raise_for_status = MagicMock() mock_get.return_value = mock_response - result = asyncio.run(fetch_rss("https://example.com/atom")) + result = await fetch_rss("https://example.com/atom") assert result is not None assert len(result) == 1 assert result[0]["title"] == "Atom статья 1" @@ -73,7 +73,7 @@ class TestFetchRss: assert result[0]["tags"] == ["AI"] @patch("utils.news._session.get") - def test_fetch_rss_empty_items(self, mock_get): + async def test_fetch_rss_empty_items(self, mock_get): """RSS без items должен вернуть пустой список.""" rss_content = """ @@ -84,11 +84,11 @@ class TestFetchRss: mock_response.content = rss_content mock_response.raise_for_status = MagicMock() mock_get.return_value = mock_response - result = asyncio.run(fetch_rss("https://example.com/rss")) + result = await fetch_rss("https://example.com/rss") assert result == [] @patch("utils.news._session.get") - def test_fetch_rss_no_matching_format(self, mock_get): + async def test_fetch_rss_no_matching_format(self, mock_get): """Неизвестный формат XML должен вернуть пустой список.""" xml_content = """ """.encode() @@ -96,11 +96,11 @@ class TestFetchRss: mock_response.content = xml_content mock_response.raise_for_status = MagicMock() mock_get.return_value = mock_response - result = asyncio.run(fetch_rss("https://example.com/xml")) + result = await fetch_rss("https://example.com/xml") assert result == [] @patch("utils.news._session.get") - def test_fetch_rss_missing_title(self, mock_get): + async def test_fetch_rss_missing_title(self, mock_get): """Статья без title должна получить 'Без названия'.""" rss_content = """ @@ -116,7 +116,7 @@ class TestFetchRss: mock_response.content = rss_content mock_response.raise_for_status = MagicMock() mock_get.return_value = mock_response - result = asyncio.run(fetch_rss("https://example.com/rss")) + result = await fetch_rss("https://example.com/rss") assert result is not None assert result[0]["title"] == "Без title" assert result[0]["link"] == "https://habr.com/1" @@ -125,7 +125,7 @@ class TestFetchRss: assert result[0]["tags"] == [] @patch("utils.news._session.get") - def test_fetch_rss_missing_guid(self, mock_get): + async def test_fetch_rss_missing_guid(self, mock_get): """Статья без guid isPermaLink должна иметь пустую ссылку.""" rss_content = """ @@ -141,13 +141,13 @@ class TestFetchRss: mock_response.content = rss_content mock_response.raise_for_status = MagicMock() mock_get.return_value = mock_response - result = asyncio.run(fetch_rss("https://example.com/rss")) + result = await fetch_rss("https://example.com/rss") assert result is not None assert result[0]["title"] == "Без guid" assert result[0]["link"] == "" @patch("utils.news._session.get") - def test_fetch_rss_limit_to_10(self, mock_get): + async def test_fetch_rss_limit_to_10(self, mock_get): """Больше 10 items должно быть обрезано до 10.""" items = "\n".join( f""" @@ -167,46 +167,46 @@ class TestFetchRss: mock_response.content = rss_content mock_response.raise_for_status = MagicMock() mock_get.return_value = mock_response - result = asyncio.run(fetch_rss("https://example.com/rss")) + result = await fetch_rss("https://example.com/rss") assert result is not None assert len(result) == 10 assert result[0]["title"] == "Статья 0" assert result[9]["title"] == "Статья 9" @patch("utils.news._session.get") - def test_fetch_rss_http_error(self, mock_get): + async def test_fetch_rss_http_error(self, mock_get): """HTTP-ошибка должна вернуть None.""" import requests mock_get.side_effect = requests.exceptions.HTTPError("404 Not Found") - result = asyncio.run(fetch_rss("https://example.com/rss")) + result = await fetch_rss("https://example.com/rss") assert result is None @patch("utils.news._session.get") - def test_fetch_rss_connection_error(self, mock_get): + async def test_fetch_rss_connection_error(self, mock_get): """Ошибка соединения должна вернуть None.""" import requests mock_get.side_effect = requests.exceptions.ConnectionError("No connection") - result = asyncio.run(fetch_rss("https://example.com/rss")) + result = await fetch_rss("https://example.com/rss") assert result is None @patch("utils.news._session.get") - def test_fetch_rss_timeout(self, mock_get): + async def test_fetch_rss_timeout(self, mock_get): """Таймаут должен вернуть None.""" import requests mock_get.side_effect = requests.exceptions.Timeout("Request timed out") - result = asyncio.run(fetch_rss("https://example.com/rss")) + result = await fetch_rss("https://example.com/rss") assert result is None @patch("utils.news._session.get") - def test_fetch_rss_ssl_error(self, mock_get): + async def test_fetch_rss_ssl_error(self, mock_get): """SSLError должен вернуть None.""" import requests mock_get.side_effect = requests.exceptions.SSLError("SSL handshake failed") - result = asyncio.run(fetch_rss("https://example.com/rss")) + result = await fetch_rss("https://example.com/rss") assert result is None @patch("utils.news._session.get") - def test_fetch_rss_empty_tags(self, mock_get): + async def test_fetch_rss_empty_tags(self, mock_get): """Статья с пустыми тегами должна иметь пустые строки.""" rss_content = """ @@ -222,7 +222,7 @@ class TestFetchRss: mock_response.content = rss_content mock_response.raise_for_status = MagicMock() mock_get.return_value = mock_response - result = asyncio.run(fetch_rss("https://example.com/rss")) + result = await fetch_rss("https://example.com/rss") assert result is not None assert result[0]["title"] == "Пустые теги" assert result[0]["link"] == "https://habr.com/1" @@ -231,7 +231,7 @@ class TestFetchRss: assert result[0]["tags"] == [] @patch("utils.news._session.get") - def test_fetch_rss_category_without_text(self, mock_get): + async def test_fetch_rss_category_without_text(self, mock_get): """Категория без текста должна быть пропущена.""" rss_content = """ @@ -248,12 +248,12 @@ class TestFetchRss: mock_response.content = rss_content mock_response.raise_for_status = MagicMock() mock_get.return_value = mock_response - result = asyncio.run(fetch_rss("https://example.com/rss")) + result = await fetch_rss("https://example.com/rss") assert result is not None assert result[0]["tags"] == ["AI"] @patch("utils.news._session.get") - def test_fetch_rss_atom_missing_author(self, mock_get): + async def test_fetch_rss_atom_missing_author(self, mock_get): """Atom feed без автора должен иметь пустого creator.""" atom_content = """ @@ -267,13 +267,13 @@ class TestFetchRss: mock_response.content = atom_content mock_response.raise_for_status = MagicMock() mock_get.return_value = mock_response - result = asyncio.run(fetch_rss("https://example.com/atom")) + result = await fetch_rss("https://example.com/atom") assert result is not None assert result[0]["title"] == "Без автора" assert result[0]["creator"] == "" @patch("utils.news._session.get") - def test_fetch_rss_atom_missing_link(self, mock_get): + async def test_fetch_rss_atom_missing_link(self, mock_get): """Atom feed без link должен иметь пустую ссылку.""" atom_content = """ @@ -286,21 +286,21 @@ class TestFetchRss: mock_response.content = atom_content mock_response.raise_for_status = MagicMock() mock_get.return_value = mock_response - result = asyncio.run(fetch_rss("https://example.com/atom")) + result = await fetch_rss("https://example.com/atom") assert result is not None assert result[0]["title"] == "Без ссылки" assert result[0]["link"] == "" @patch("utils.news._session.get") - def test_fetch_rss_request_exception(self, mock_get): + async def test_fetch_rss_request_exception(self, mock_get): """Общий RequestException должен вернуть None.""" import requests mock_get.side_effect = requests.RequestException("Generic error") - result = asyncio.run(fetch_rss("https://example.com/rss")) + result = await fetch_rss("https://example.com/rss") assert result is None @patch("utils.news._session.get") - def test_fetch_rss_guid_fallback_to_link(self, mock_get): + async def test_fetch_rss_guid_fallback_to_link(self, mock_get): """Если нет guid isPermaLink, ссылка должна быть пустой.""" rss_content = """ @@ -316,12 +316,12 @@ class TestFetchRss: mock_response.content = rss_content mock_response.raise_for_status = MagicMock() mock_get.return_value = mock_response - result = asyncio.run(fetch_rss("https://example.com/rss")) + result = await fetch_rss("https://example.com/rss") assert result is not None assert result[0]["link"] == "" @patch("utils.news._session.get") - def test_fetch_rss_single_item(self, mock_get): + async def test_fetch_rss_single_item(self, mock_get): """Один item должен быть распарсен корректно.""" rss_content = """ @@ -339,7 +339,7 @@ class TestFetchRss: mock_response.content = rss_content mock_response.raise_for_status = MagicMock() mock_get.return_value = mock_response - result = asyncio.run(fetch_rss("https://example.com/rss")) + result = await fetch_rss("https://example.com/rss") assert result is not None assert len(result) == 1 assert result[0]["title"] == "Единственная статья" @@ -348,7 +348,7 @@ class TestFetchRss: assert result[0]["tags"] == ["ML"] @patch("utils.news._session.get") - def test_fetch_rss_special_characters_in_title(self, mock_get): + async def test_fetch_rss_special_characters_in_title(self, mock_get): """Заголовки со спецсимволами должны парситься корректно.""" rss_content = """ @@ -364,13 +364,13 @@ class TestFetchRss: mock_response.content = rss_content mock_response.raise_for_status = MagicMock() mock_get.return_value = mock_response - result = asyncio.run(fetch_rss("https://example.com/rss")) + result = await fetch_rss("https://example.com/rss") assert result is not None assert "AI" in result[0]["title"] assert "ML" in result[0]["title"] @patch("utils.news._session.get") - def test_fetch_rss_date_with_gmt(self, mock_get): + async def test_fetch_rss_date_with_gmt(self, mock_get): """Дата с GMT должна парситься корректно.""" rss_content = """ @@ -386,12 +386,12 @@ class TestFetchRss: mock_response.content = rss_content mock_response.raise_for_status = MagicMock() mock_get.return_value = mock_response - result = asyncio.run(fetch_rss("https://example.com/rss")) + result = await fetch_rss("https://example.com/rss") assert result is not None assert result[0]["pub_date"] == "Mon, 28 May 2026 10:00:00 GMT" @patch("utils.news._session.get") - def test_fetch_rss_many_categories(self, mock_get): + async def test_fetch_rss_many_categories(self, mock_get): """Множество категорий должны быть собраны.""" rss_content = """ @@ -411,6 +411,6 @@ class TestFetchRss: mock_response.content = rss_content mock_response.raise_for_status = MagicMock() mock_get.return_value = mock_response - result = asyncio.run(fetch_rss("https://example.com/rss")) + result = await fetch_rss("https://example.com/rss") assert result is not None assert result[0]["tags"] == ["AI", "ML", "Deep Learning", "NLP", "Computer Vision"] diff --git a/tests/test_fetch_weather.py b/tests/test_fetch_weather.py index 361ebb3..956b1bd 100644 --- a/tests/test_fetch_weather.py +++ b/tests/test_fetch_weather.py @@ -1,4 +1,4 @@ -import asyncio + import pytest from unittest.mock import patch, MagicMock from utils.pogoda import fetch_weather, fetch_open_meteo @@ -8,17 +8,17 @@ class TestFetchWeather: """Тесты функции fetch_weather() — получение погоды с retry-логикой.""" @patch("utils.pogoda._session.get") - def test_fetch_weather_success(self, mock_get): + async def test_fetch_weather_success(self, mock_get): """Успешный ответ должен вернуть JSON-данные.""" mock_response = MagicMock() mock_response.json.return_value = {"current_condition": [{"temp_C": 20}]} mock_response.raise_for_status = MagicMock() mock_get.return_value = mock_response - result = asyncio.run(fetch_weather("https://test.example.com")) + result = await fetch_weather("https://test.example.com") assert result == {"current_condition": [{"temp_C": 20}]} @patch("utils.pogoda._session.get") - def test_fetch_weather_fallback_on_ssl_error(self, mock_get): + async def test_fetch_weather_fallback_on_ssl_error(self, mock_get): """SSLError на первой попытке → fallback на Open-Meteo.""" from requests.exceptions import SSLError mock_get.side_effect = [ @@ -27,64 +27,64 @@ class TestFetchWeather: ] with patch("utils.pogoda.fetch_open_meteo") as mock_fallback: mock_fallback.return_value = {"result": "fallback"} - result = asyncio.run(fetch_weather("https://test.example.com")) + result = await fetch_weather("https://test.example.com") assert result == {"result": "fallback"} @patch("utils.pogoda._session.get") - def test_fetch_weather_fallback_on_connection_error(self, mock_get): + async def test_fetch_weather_fallback_on_connection_error(self, mock_get): """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"} - result = asyncio.run(fetch_weather("https://test.example.com")) + result = await fetch_weather("https://test.example.com") assert result == {"result": "fallback"} @patch("utils.pogoda._session.get") - def test_fetch_weather_fallback_on_timeout(self, mock_get): + async def test_fetch_weather_fallback_on_timeout(self, mock_get): """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"} - result = asyncio.run(fetch_weather("https://test.example.com")) + result = await fetch_weather("https://test.example.com") assert result == {"result": "fallback"} @patch("utils.pogoda._session.get") - def test_fetch_weather_all_retries_fail(self, mock_get): + async def test_fetch_weather_all_retries_fail(self, mock_get): """Все попытки не удались → 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 - result = asyncio.run(fetch_weather("https://test.example.com")) + result = await fetch_weather("https://test.example.com") assert result is None @patch("utils.pogoda._session.get") - def test_fetch_weather_request_exception(self, mock_get): + async def test_fetch_weather_request_exception(self, mock_get): """Общий 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"} - result = asyncio.run(fetch_weather("https://test.example.com")) + result = await fetch_weather("https://test.example.com") assert result == {"result": "fallback"} @patch("utils.pogoda._session.get") - def test_fetch_weather_http_error_no_fallback(self, mock_get): + async def test_fetch_weather_http_error_no_fallback(self, mock_get): """HTTP-ошибка (raise_for_status) не ловится, падает.""" mock_response = MagicMock() mock_response.raise_for_status.side_effect = Exception("HTTP 500") mock_get.return_value = mock_response with pytest.raises(Exception): - asyncio.run(fetch_weather("https://test.example.com")) + await fetch_weather("https://test.example.com") class TestFetchOpenMeteo: """Тесты функции fetch_open_meteo() — fallback на Open-Meteo API.""" @patch("utils.pogoda._session.get") - def test_fetch_open_meteo_success(self, mock_get): + async def test_fetch_open_meteo_success(self, mock_get): """Успешный ответ должен вернуть данные в формате current_condition.""" mock_response = MagicMock() mock_response.json.return_value = { @@ -99,7 +99,7 @@ class TestFetchOpenMeteo: } mock_response.raise_for_status = MagicMock() mock_get.return_value = mock_response - result = asyncio.run(fetch_open_meteo()) + result = await fetch_open_meteo() assert result is not None assert "current_condition" in result assert result["current_condition"][0]["temp_C"] == 15 @@ -108,13 +108,13 @@ class TestFetchOpenMeteo: assert result["current_condition"][0]["pressure"] == 1013 @patch("utils.pogoda._session.get") - def test_fetch_open_meteo_custom_coords(self, mock_get): + async def test_fetch_open_meteo_custom_coords(self, mock_get): """Кастомные координаты должны быть в 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}} mock_response.raise_for_status = MagicMock() mock_get.return_value = mock_response - result = asyncio.run(fetch_open_meteo(lat=55.7558, lon=37.6173)) + result = await fetch_open_meteo(lat=55.7558, lon=37.6173) assert result is not None mock_get.assert_called_once() call_url = mock_get.call_args[0][0] @@ -122,95 +122,95 @@ class TestFetchOpenMeteo: assert "37.6173" in call_url @patch("utils.pogoda._session.get") - def test_fetch_open_meteo_missing_weather_code(self, mock_get): + async def test_fetch_open_meteo_missing_weather_code(self, mock_get): """Отсутствующий weather_code → 'Неизвестно'.""" mock_response = MagicMock() mock_response.json.return_value = {"current": {"temperature": 10}} mock_response.raise_for_status = MagicMock() mock_get.return_value = mock_response - result = asyncio.run(fetch_open_meteo()) + result = await fetch_open_meteo() assert result is not None assert result["current_condition"][0]["weatherDesc"] == [{"value": "Неизвестно"}] @patch("utils.pogoda._session.get") - def test_fetch_open_meteo_ssl_error(self, mock_get): + async def test_fetch_open_meteo_ssl_error(self, mock_get): """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 pass - result = asyncio.run(fetch_open_meteo()) + result = await fetch_open_meteo() assert result is None @patch("utils.pogoda._session.get") - def test_fetch_open_meteo_connection_error(self, mock_get): + async def test_fetch_open_meteo_connection_error(self, mock_get): """ConnectionError → вернуть None.""" from requests.exceptions import ConnectionError mock_get.side_effect = ConnectionError("No connection") - result = asyncio.run(fetch_open_meteo()) + result = await fetch_open_meteo() assert result is None @patch("utils.pogoda._session.get") - def test_fetch_open_meteo_timeout(self, mock_get): + async def test_fetch_open_meteo_timeout(self, mock_get): """Timeout → вернуть None.""" from requests.exceptions import Timeout mock_get.side_effect = Timeout("Timed out") - result = asyncio.run(fetch_open_meteo()) + result = await fetch_open_meteo() assert result is None @patch("utils.pogoda._session.get") - def test_fetch_open_meteo_request_exception(self, mock_get): + async def test_fetch_open_meteo_request_exception(self, mock_get): """Общий RequestException → вернуть None.""" import requests mock_get.side_effect = requests.RequestException("Error") - result = asyncio.run(fetch_open_meteo()) + result = await fetch_open_meteo() assert result is None @patch("utils.pogoda._session.get") - def test_fetch_open_meteo_json_parse_error(self, mock_get): + async def test_fetch_open_meteo_json_parse_error(self, mock_get): """Ошибка парсинга JSON → вернуть None.""" import requests mock_response = MagicMock() mock_response.json.side_effect = requests.RequestException("JSON Error") mock_response.raise_for_status = MagicMock() mock_get.return_value = mock_response - result = asyncio.run(fetch_open_meteo()) + result = await fetch_open_meteo() assert result is None @patch("utils.pogoda._session.get") - def test_fetch_open_meteo_retry_on_error(self, mock_get): + async def test_fetch_open_meteo_retry_on_error(self, mock_get): """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() mock_get.side_effect = [ConnectionError("fail"), success_response] - result = asyncio.run(fetch_open_meteo(max_retries=2)) + result = await fetch_open_meteo(max_retries=2) assert result is not None assert mock_get.call_count == 2 @patch("utils.pogoda._session.get") - def test_fetch_open_meteo_all_retries_fail(self, mock_get): + async def test_fetch_open_meteo_all_retries_fail(self, mock_get): """Все попытки неудачны → None.""" from requests.exceptions import ConnectionError mock_get.side_effect = [ConnectionError("fail"), ConnectionError("fail"), ConnectionError("fail")] - result = asyncio.run(fetch_open_meteo(max_retries=3)) + result = await fetch_open_meteo(max_retries=3) assert result is None assert mock_get.call_count == 3 @patch("utils.pogoda._session.get") - def test_fetch_open_meteo_http_error(self, mock_get): + async def test_fetch_open_meteo_http_error(self, mock_get): """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 - result = asyncio.run(fetch_open_meteo()) + result = await fetch_open_meteo() assert result is None @patch("utils.pogoda._session.get") - def test_fetch_open_meteo_wind_speed_0(self, mock_get): + async def test_fetch_open_meteo_wind_speed_0(self, mock_get): """Нулевая скорость ветра должна корректно обрабатываться.""" mock_response = MagicMock() mock_response.json.return_value = { @@ -225,7 +225,7 @@ class TestFetchOpenMeteo: } mock_response.raise_for_status = MagicMock() mock_get.return_value = mock_response - result = asyncio.run(fetch_open_meteo()) + result = await fetch_open_meteo() assert result is not None assert result["current_condition"][0]["windspeedKmph"] == 0 assert result["current_condition"][0]["pressure"] == 1000