fix: H5 — 50 тестов asyncio.run() заменены на async def
- test_fetch_cat.py: 10 тестов (def → async def, asyncio.run → await) - test_fetch_rss.py: 21 тест (def → async def, asyncio.run → await) - test_fetch_weather.py: 19 тест (def → async def, asyncio.run → await) - Удалён лишний import asyncio из тестовых файлов - Больше не создаётся новый event loop на каждый тест - RateLimiter (asyncio.Lock) теперь корректно работает в одном loop 230 тестов проходят.
This commit is contained in:
parent
e1ab1e3a61
commit
6e2a092548
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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 = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0">
|
||||
@ -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 = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
@ -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 = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0">
|
||||
@ -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 = """<?xml version="1.0"?>
|
||||
<unknown></unknown>""".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 = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0">
|
||||
@ -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 = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0">
|
||||
@ -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""" <item>
|
||||
@ -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 = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0">
|
||||
@ -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 = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0">
|
||||
@ -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 = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
@ -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 = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
@ -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 = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0">
|
||||
@ -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 = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0">
|
||||
@ -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 = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0">
|
||||
@ -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 = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0">
|
||||
@ -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 = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0">
|
||||
@ -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"]
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user