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 вхождений
|
- `tests/test_fetch_cat.py` -- 10 вхождений
|
||||||
@ -540,7 +540,7 @@ DeprecationWarning: 'asyncio.iscoroutinefunction' is deprecated and slated for r
|
|||||||
4. [x] H2: Отсортировать ключи `translate_weather` по убыванию длины — **выполнено**
|
4. [x] H2: Отсортировать ключи `translate_weather` по убыванию длины — **выполнено**
|
||||||
5. [x] H3: Исправить `pressure_to_mmhg` -- `not mb` -> `mb is None or mb == ""` — **выполнено**
|
5. [x] H3: Исправить `pressure_to_mmhg` -- `not mb` -> `mb is None or mb == ""` — **выполнено**
|
||||||
6. [x] H4: Добавить защиту от дублирования cogs в `on_ready` — **выполнено**
|
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`
|
8. [ ] H6: Добавить HTTPError в retry-блок `fetch_weather`
|
||||||
9. [ ] H7: Добавить HTTPException/BadRequest в `on_command_error`
|
9. [ ] H7: Добавить HTTPException/BadRequest в `on_command_error`
|
||||||
10. [ ] H8: Исправить утечку корутины в тестах Scheduler
|
10. [ ] H8: Исправить утечку корутины в тестах Scheduler
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import asyncio
|
|
||||||
import json
|
import json
|
||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
@ -9,94 +8,94 @@ class TestFetchCat:
|
|||||||
"""Тесты функции fetch_cat() — получение URL случайного котика."""
|
"""Тесты функции fetch_cat() — получение URL случайного котика."""
|
||||||
|
|
||||||
@patch("utils.cat._session.get")
|
@patch("utils.cat._session.get")
|
||||||
def test_fetch_cat_success(self, mock_get):
|
async def test_fetch_cat_success(self, mock_get):
|
||||||
"""Успешный ответ с URL должен вернуть строку."""
|
"""Успешный ответ с URL должен вернуть строку."""
|
||||||
mock_response = MagicMock()
|
mock_response = MagicMock()
|
||||||
mock_response.json.return_value = [{"url": "https://example.com/cat.jpg"}]
|
mock_response.json.return_value = [{"url": "https://example.com/cat.jpg"}]
|
||||||
mock_response.raise_for_status = MagicMock()
|
mock_response.raise_for_status = MagicMock()
|
||||||
mock_get.return_value = mock_response
|
mock_get.return_value = mock_response
|
||||||
result = asyncio.run(fetch_cat())
|
result = await fetch_cat()
|
||||||
assert result == "https://example.com/cat.jpg"
|
assert result == "https://example.com/cat.jpg"
|
||||||
|
|
||||||
@patch("utils.cat._session.get")
|
@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."""
|
"""Пустой массив должен вернуть None."""
|
||||||
mock_response = MagicMock()
|
mock_response = MagicMock()
|
||||||
mock_response.json.return_value = []
|
mock_response.json.return_value = []
|
||||||
mock_response.raise_for_status = MagicMock()
|
mock_response.raise_for_status = MagicMock()
|
||||||
mock_get.return_value = mock_response
|
mock_get.return_value = mock_response
|
||||||
result = asyncio.run(fetch_cat())
|
result = await fetch_cat()
|
||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
@patch("utils.cat._session.get")
|
@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."""
|
"""HTTP-ошибка (raise_for_status) должна вернуть None."""
|
||||||
import requests
|
import requests
|
||||||
mock_response = MagicMock()
|
mock_response = MagicMock()
|
||||||
mock_response.raise_for_status.side_effect = requests.HTTPError("404 Not Found")
|
mock_response.raise_for_status.side_effect = requests.HTTPError("404 Not Found")
|
||||||
mock_get.return_value = mock_response
|
mock_get.return_value = mock_response
|
||||||
result = asyncio.run(fetch_cat())
|
result = await fetch_cat()
|
||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
@patch("utils.cat._session.get")
|
@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."""
|
"""ConnectionError должна вернуть None."""
|
||||||
from requests.exceptions import ConnectionError
|
from requests.exceptions import ConnectionError
|
||||||
mock_get.side_effect = ConnectionError("No connection")
|
mock_get.side_effect = ConnectionError("No connection")
|
||||||
result = asyncio.run(fetch_cat())
|
result = await fetch_cat()
|
||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
@patch("utils.cat._session.get")
|
@patch("utils.cat._session.get")
|
||||||
def test_fetch_cat_timeout(self, mock_get):
|
async def test_fetch_cat_timeout(self, mock_get):
|
||||||
"""Timeout должна вернуть None."""
|
"""Timeout должна вернуть None."""
|
||||||
from requests.exceptions import Timeout
|
from requests.exceptions import Timeout
|
||||||
mock_get.side_effect = Timeout("Request timed out")
|
mock_get.side_effect = Timeout("Request timed out")
|
||||||
result = asyncio.run(fetch_cat())
|
result = await fetch_cat()
|
||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
@patch("utils.cat._session.get")
|
@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."""
|
"""SSLError должна вернуть None."""
|
||||||
from requests.exceptions import SSLError
|
from requests.exceptions import SSLError
|
||||||
mock_get.side_effect = SSLError("SSL handshake failed")
|
mock_get.side_effect = SSLError("SSL handshake failed")
|
||||||
result = asyncio.run(fetch_cat())
|
result = await fetch_cat()
|
||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
@patch("utils.cat._session.get")
|
@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."""
|
"""Ошибка парсинга JSON должна вернуть None."""
|
||||||
import requests
|
import requests
|
||||||
mock_response = MagicMock()
|
mock_response = MagicMock()
|
||||||
mock_response.json.side_effect = requests.JSONDecodeError("Expecting value", "", 0)
|
mock_response.json.side_effect = requests.JSONDecodeError("Expecting value", "", 0)
|
||||||
mock_response.raise_for_status = MagicMock()
|
mock_response.raise_for_status = MagicMock()
|
||||||
mock_get.return_value = mock_response
|
mock_get.return_value = mock_response
|
||||||
result = asyncio.run(fetch_cat())
|
result = await fetch_cat()
|
||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
@patch("utils.cat._session.get")
|
@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."""
|
"""Отсутствие ключа 'url' в ответе должно вернуть None."""
|
||||||
mock_response = MagicMock()
|
mock_response = MagicMock()
|
||||||
mock_response.json.return_value = [{"error": "no image"}]
|
mock_response.json.return_value = [{"error": "no image"}]
|
||||||
mock_response.raise_for_status = MagicMock()
|
mock_response.raise_for_status = MagicMock()
|
||||||
mock_get.return_value = mock_response
|
mock_get.return_value = mock_response
|
||||||
result = asyncio.run(fetch_cat())
|
result = await fetch_cat()
|
||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
@patch("utils.cat._session.get")
|
@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."""
|
"""Общий RequestException должен вернуть None."""
|
||||||
import requests
|
import requests
|
||||||
mock_get.side_effect = requests.RequestException("Generic error")
|
mock_get.side_effect = requests.RequestException("Generic error")
|
||||||
result = asyncio.run(fetch_cat())
|
result = await fetch_cat()
|
||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
@patch("utils.cat._session.get")
|
@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 со спецсимволами должен вернуться как есть."""
|
"""URL со спецсимволами должен вернуться как есть."""
|
||||||
mock_response = MagicMock()
|
mock_response = MagicMock()
|
||||||
mock_response.json.return_value = [{"url": "https://example.com/cat?w=100&h=200"}]
|
mock_response.json.return_value = [{"url": "https://example.com/cat?w=100&h=200"}]
|
||||||
mock_response.raise_for_status = MagicMock()
|
mock_response.raise_for_status = MagicMock()
|
||||||
mock_get.return_value = mock_response
|
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"
|
assert result == "https://example.com/cat?w=100&h=200"
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import asyncio
|
|
||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
from utils.news import fetch_rss
|
from utils.news import fetch_rss
|
||||||
@ -8,7 +8,7 @@ class TestFetchRss:
|
|||||||
"""Тесты функции fetch_rss() — получение и парсинг RSS-ленты."""
|
"""Тесты функции fetch_rss() — получение и парсинг RSS-ленты."""
|
||||||
|
|
||||||
@patch("utils.news._session.get")
|
@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 2.0 должен вернуть список статей."""
|
||||||
rss_content = """<?xml version="1.0" encoding="UTF-8"?>
|
rss_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<rss version="2.0">
|
<rss version="2.0">
|
||||||
@ -34,7 +34,7 @@ class TestFetchRss:
|
|||||||
mock_response.content = rss_content
|
mock_response.content = rss_content
|
||||||
mock_response.raise_for_status = MagicMock()
|
mock_response.raise_for_status = MagicMock()
|
||||||
mock_get.return_value = mock_response
|
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 is not None
|
||||||
assert len(result) == 2
|
assert len(result) == 2
|
||||||
assert result[0]["title"] == "Статья 1"
|
assert result[0]["title"] == "Статья 1"
|
||||||
@ -47,7 +47,7 @@ class TestFetchRss:
|
|||||||
assert result[1]["tags"] == []
|
assert result[1]["tags"] == []
|
||||||
|
|
||||||
@patch("utils.news._session.get")
|
@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 должен вернуть список статей."""
|
||||||
atom_content = """<?xml version="1.0" encoding="UTF-8"?>
|
atom_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
@ -63,7 +63,7 @@ class TestFetchRss:
|
|||||||
mock_response.content = atom_content
|
mock_response.content = atom_content
|
||||||
mock_response.raise_for_status = MagicMock()
|
mock_response.raise_for_status = MagicMock()
|
||||||
mock_get.return_value = mock_response
|
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 is not None
|
||||||
assert len(result) == 1
|
assert len(result) == 1
|
||||||
assert result[0]["title"] == "Atom статья 1"
|
assert result[0]["title"] == "Atom статья 1"
|
||||||
@ -73,7 +73,7 @@ class TestFetchRss:
|
|||||||
assert result[0]["tags"] == ["AI"]
|
assert result[0]["tags"] == ["AI"]
|
||||||
|
|
||||||
@patch("utils.news._session.get")
|
@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 без items должен вернуть пустой список."""
|
||||||
rss_content = """<?xml version="1.0" encoding="UTF-8"?>
|
rss_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<rss version="2.0">
|
<rss version="2.0">
|
||||||
@ -84,11 +84,11 @@ class TestFetchRss:
|
|||||||
mock_response.content = rss_content
|
mock_response.content = rss_content
|
||||||
mock_response.raise_for_status = MagicMock()
|
mock_response.raise_for_status = MagicMock()
|
||||||
mock_get.return_value = mock_response
|
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 == []
|
assert result == []
|
||||||
|
|
||||||
@patch("utils.news._session.get")
|
@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 должен вернуть пустой список."""
|
||||||
xml_content = """<?xml version="1.0"?>
|
xml_content = """<?xml version="1.0"?>
|
||||||
<unknown></unknown>""".encode()
|
<unknown></unknown>""".encode()
|
||||||
@ -96,11 +96,11 @@ class TestFetchRss:
|
|||||||
mock_response.content = xml_content
|
mock_response.content = xml_content
|
||||||
mock_response.raise_for_status = MagicMock()
|
mock_response.raise_for_status = MagicMock()
|
||||||
mock_get.return_value = mock_response
|
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 == []
|
assert result == []
|
||||||
|
|
||||||
@patch("utils.news._session.get")
|
@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 должна получить 'Без названия'."""
|
"""Статья без title должна получить 'Без названия'."""
|
||||||
rss_content = """<?xml version="1.0" encoding="UTF-8"?>
|
rss_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<rss version="2.0">
|
<rss version="2.0">
|
||||||
@ -116,7 +116,7 @@ class TestFetchRss:
|
|||||||
mock_response.content = rss_content
|
mock_response.content = rss_content
|
||||||
mock_response.raise_for_status = MagicMock()
|
mock_response.raise_for_status = MagicMock()
|
||||||
mock_get.return_value = mock_response
|
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 is not None
|
||||||
assert result[0]["title"] == "Без title"
|
assert result[0]["title"] == "Без title"
|
||||||
assert result[0]["link"] == "https://habr.com/1"
|
assert result[0]["link"] == "https://habr.com/1"
|
||||||
@ -125,7 +125,7 @@ class TestFetchRss:
|
|||||||
assert result[0]["tags"] == []
|
assert result[0]["tags"] == []
|
||||||
|
|
||||||
@patch("utils.news._session.get")
|
@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 должна иметь пустую ссылку."""
|
"""Статья без guid isPermaLink должна иметь пустую ссылку."""
|
||||||
rss_content = """<?xml version="1.0" encoding="UTF-8"?>
|
rss_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<rss version="2.0">
|
<rss version="2.0">
|
||||||
@ -141,13 +141,13 @@ class TestFetchRss:
|
|||||||
mock_response.content = rss_content
|
mock_response.content = rss_content
|
||||||
mock_response.raise_for_status = MagicMock()
|
mock_response.raise_for_status = MagicMock()
|
||||||
mock_get.return_value = mock_response
|
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 is not None
|
||||||
assert result[0]["title"] == "Без guid"
|
assert result[0]["title"] == "Без guid"
|
||||||
assert result[0]["link"] == ""
|
assert result[0]["link"] == ""
|
||||||
|
|
||||||
@patch("utils.news._session.get")
|
@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."""
|
"""Больше 10 items должно быть обрезано до 10."""
|
||||||
items = "\n".join(
|
items = "\n".join(
|
||||||
f""" <item>
|
f""" <item>
|
||||||
@ -167,46 +167,46 @@ class TestFetchRss:
|
|||||||
mock_response.content = rss_content
|
mock_response.content = rss_content
|
||||||
mock_response.raise_for_status = MagicMock()
|
mock_response.raise_for_status = MagicMock()
|
||||||
mock_get.return_value = mock_response
|
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 is not None
|
||||||
assert len(result) == 10
|
assert len(result) == 10
|
||||||
assert result[0]["title"] == "Статья 0"
|
assert result[0]["title"] == "Статья 0"
|
||||||
assert result[9]["title"] == "Статья 9"
|
assert result[9]["title"] == "Статья 9"
|
||||||
|
|
||||||
@patch("utils.news._session.get")
|
@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."""
|
"""HTTP-ошибка должна вернуть None."""
|
||||||
import requests
|
import requests
|
||||||
mock_get.side_effect = requests.exceptions.HTTPError("404 Not Found")
|
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
|
assert result is None
|
||||||
|
|
||||||
@patch("utils.news._session.get")
|
@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."""
|
"""Ошибка соединения должна вернуть None."""
|
||||||
import requests
|
import requests
|
||||||
mock_get.side_effect = requests.exceptions.ConnectionError("No connection")
|
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
|
assert result is None
|
||||||
|
|
||||||
@patch("utils.news._session.get")
|
@patch("utils.news._session.get")
|
||||||
def test_fetch_rss_timeout(self, mock_get):
|
async def test_fetch_rss_timeout(self, mock_get):
|
||||||
"""Таймаут должен вернуть None."""
|
"""Таймаут должен вернуть None."""
|
||||||
import requests
|
import requests
|
||||||
mock_get.side_effect = requests.exceptions.Timeout("Request timed out")
|
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
|
assert result is None
|
||||||
|
|
||||||
@patch("utils.news._session.get")
|
@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."""
|
"""SSLError должен вернуть None."""
|
||||||
import requests
|
import requests
|
||||||
mock_get.side_effect = requests.exceptions.SSLError("SSL handshake failed")
|
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
|
assert result is None
|
||||||
|
|
||||||
@patch("utils.news._session.get")
|
@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_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<rss version="2.0">
|
<rss version="2.0">
|
||||||
@ -222,7 +222,7 @@ class TestFetchRss:
|
|||||||
mock_response.content = rss_content
|
mock_response.content = rss_content
|
||||||
mock_response.raise_for_status = MagicMock()
|
mock_response.raise_for_status = MagicMock()
|
||||||
mock_get.return_value = mock_response
|
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 is not None
|
||||||
assert result[0]["title"] == "Пустые теги"
|
assert result[0]["title"] == "Пустые теги"
|
||||||
assert result[0]["link"] == "https://habr.com/1"
|
assert result[0]["link"] == "https://habr.com/1"
|
||||||
@ -231,7 +231,7 @@ class TestFetchRss:
|
|||||||
assert result[0]["tags"] == []
|
assert result[0]["tags"] == []
|
||||||
|
|
||||||
@patch("utils.news._session.get")
|
@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_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<rss version="2.0">
|
<rss version="2.0">
|
||||||
@ -248,12 +248,12 @@ class TestFetchRss:
|
|||||||
mock_response.content = rss_content
|
mock_response.content = rss_content
|
||||||
mock_response.raise_for_status = MagicMock()
|
mock_response.raise_for_status = MagicMock()
|
||||||
mock_get.return_value = mock_response
|
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 is not None
|
||||||
assert result[0]["tags"] == ["AI"]
|
assert result[0]["tags"] == ["AI"]
|
||||||
|
|
||||||
@patch("utils.news._session.get")
|
@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 feed без автора должен иметь пустого creator."""
|
||||||
atom_content = """<?xml version="1.0" encoding="UTF-8"?>
|
atom_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
@ -267,13 +267,13 @@ class TestFetchRss:
|
|||||||
mock_response.content = atom_content
|
mock_response.content = atom_content
|
||||||
mock_response.raise_for_status = MagicMock()
|
mock_response.raise_for_status = MagicMock()
|
||||||
mock_get.return_value = mock_response
|
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 is not None
|
||||||
assert result[0]["title"] == "Без автора"
|
assert result[0]["title"] == "Без автора"
|
||||||
assert result[0]["creator"] == ""
|
assert result[0]["creator"] == ""
|
||||||
|
|
||||||
@patch("utils.news._session.get")
|
@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 feed без link должен иметь пустую ссылку."""
|
||||||
atom_content = """<?xml version="1.0" encoding="UTF-8"?>
|
atom_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
@ -286,21 +286,21 @@ class TestFetchRss:
|
|||||||
mock_response.content = atom_content
|
mock_response.content = atom_content
|
||||||
mock_response.raise_for_status = MagicMock()
|
mock_response.raise_for_status = MagicMock()
|
||||||
mock_get.return_value = mock_response
|
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 is not None
|
||||||
assert result[0]["title"] == "Без ссылки"
|
assert result[0]["title"] == "Без ссылки"
|
||||||
assert result[0]["link"] == ""
|
assert result[0]["link"] == ""
|
||||||
|
|
||||||
@patch("utils.news._session.get")
|
@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."""
|
"""Общий RequestException должен вернуть None."""
|
||||||
import requests
|
import requests
|
||||||
mock_get.side_effect = requests.RequestException("Generic error")
|
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
|
assert result is None
|
||||||
|
|
||||||
@patch("utils.news._session.get")
|
@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, ссылка должна быть пустой."""
|
"""Если нет guid isPermaLink, ссылка должна быть пустой."""
|
||||||
rss_content = """<?xml version="1.0" encoding="UTF-8"?>
|
rss_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<rss version="2.0">
|
<rss version="2.0">
|
||||||
@ -316,12 +316,12 @@ class TestFetchRss:
|
|||||||
mock_response.content = rss_content
|
mock_response.content = rss_content
|
||||||
mock_response.raise_for_status = MagicMock()
|
mock_response.raise_for_status = MagicMock()
|
||||||
mock_get.return_value = mock_response
|
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 is not None
|
||||||
assert result[0]["link"] == ""
|
assert result[0]["link"] == ""
|
||||||
|
|
||||||
@patch("utils.news._session.get")
|
@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 должен быть распарсен корректно."""
|
"""Один item должен быть распарсен корректно."""
|
||||||
rss_content = """<?xml version="1.0" encoding="UTF-8"?>
|
rss_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<rss version="2.0">
|
<rss version="2.0">
|
||||||
@ -339,7 +339,7 @@ class TestFetchRss:
|
|||||||
mock_response.content = rss_content
|
mock_response.content = rss_content
|
||||||
mock_response.raise_for_status = MagicMock()
|
mock_response.raise_for_status = MagicMock()
|
||||||
mock_get.return_value = mock_response
|
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 is not None
|
||||||
assert len(result) == 1
|
assert len(result) == 1
|
||||||
assert result[0]["title"] == "Единственная статья"
|
assert result[0]["title"] == "Единственная статья"
|
||||||
@ -348,7 +348,7 @@ class TestFetchRss:
|
|||||||
assert result[0]["tags"] == ["ML"]
|
assert result[0]["tags"] == ["ML"]
|
||||||
|
|
||||||
@patch("utils.news._session.get")
|
@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_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<rss version="2.0">
|
<rss version="2.0">
|
||||||
@ -364,13 +364,13 @@ class TestFetchRss:
|
|||||||
mock_response.content = rss_content
|
mock_response.content = rss_content
|
||||||
mock_response.raise_for_status = MagicMock()
|
mock_response.raise_for_status = MagicMock()
|
||||||
mock_get.return_value = mock_response
|
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 is not None
|
||||||
assert "AI" in result[0]["title"]
|
assert "AI" in result[0]["title"]
|
||||||
assert "ML" in result[0]["title"]
|
assert "ML" in result[0]["title"]
|
||||||
|
|
||||||
@patch("utils.news._session.get")
|
@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 должна парситься корректно."""
|
"""Дата с GMT должна парситься корректно."""
|
||||||
rss_content = """<?xml version="1.0" encoding="UTF-8"?>
|
rss_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<rss version="2.0">
|
<rss version="2.0">
|
||||||
@ -386,12 +386,12 @@ class TestFetchRss:
|
|||||||
mock_response.content = rss_content
|
mock_response.content = rss_content
|
||||||
mock_response.raise_for_status = MagicMock()
|
mock_response.raise_for_status = MagicMock()
|
||||||
mock_get.return_value = mock_response
|
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 is not None
|
||||||
assert result[0]["pub_date"] == "Mon, 28 May 2026 10:00:00 GMT"
|
assert result[0]["pub_date"] == "Mon, 28 May 2026 10:00:00 GMT"
|
||||||
|
|
||||||
@patch("utils.news._session.get")
|
@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_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<rss version="2.0">
|
<rss version="2.0">
|
||||||
@ -411,6 +411,6 @@ class TestFetchRss:
|
|||||||
mock_response.content = rss_content
|
mock_response.content = rss_content
|
||||||
mock_response.raise_for_status = MagicMock()
|
mock_response.raise_for_status = MagicMock()
|
||||||
mock_get.return_value = mock_response
|
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 is not None
|
||||||
assert result[0]["tags"] == ["AI", "ML", "Deep Learning", "NLP", "Computer Vision"]
|
assert result[0]["tags"] == ["AI", "ML", "Deep Learning", "NLP", "Computer Vision"]
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import asyncio
|
|
||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
from utils.pogoda import fetch_weather, fetch_open_meteo
|
from utils.pogoda import fetch_weather, fetch_open_meteo
|
||||||
@ -8,17 +8,17 @@ class TestFetchWeather:
|
|||||||
"""Тесты функции fetch_weather() — получение погоды с retry-логикой."""
|
"""Тесты функции fetch_weather() — получение погоды с retry-логикой."""
|
||||||
|
|
||||||
@patch("utils.pogoda._session.get")
|
@patch("utils.pogoda._session.get")
|
||||||
def test_fetch_weather_success(self, mock_get):
|
async def test_fetch_weather_success(self, mock_get):
|
||||||
"""Успешный ответ должен вернуть JSON-данные."""
|
"""Успешный ответ должен вернуть JSON-данные."""
|
||||||
mock_response = MagicMock()
|
mock_response = MagicMock()
|
||||||
mock_response.json.return_value = {"current_condition": [{"temp_C": 20}]}
|
mock_response.json.return_value = {"current_condition": [{"temp_C": 20}]}
|
||||||
mock_response.raise_for_status = MagicMock()
|
mock_response.raise_for_status = MagicMock()
|
||||||
mock_get.return_value = mock_response
|
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}]}
|
assert result == {"current_condition": [{"temp_C": 20}]}
|
||||||
|
|
||||||
@patch("utils.pogoda._session.get")
|
@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."""
|
"""SSLError на первой попытке → fallback на Open-Meteo."""
|
||||||
from requests.exceptions import SSLError
|
from requests.exceptions import SSLError
|
||||||
mock_get.side_effect = [
|
mock_get.side_effect = [
|
||||||
@ -27,64 +27,64 @@ class TestFetchWeather:
|
|||||||
]
|
]
|
||||||
with patch("utils.pogoda.fetch_open_meteo") as mock_fallback:
|
with patch("utils.pogoda.fetch_open_meteo") as mock_fallback:
|
||||||
mock_fallback.return_value = {"result": "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"}
|
assert result == {"result": "fallback"}
|
||||||
|
|
||||||
@patch("utils.pogoda._session.get")
|
@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."""
|
"""ConnectionError → fallback на Open-Meteo."""
|
||||||
from requests.exceptions import ConnectionError
|
from requests.exceptions import ConnectionError
|
||||||
mock_get.side_effect = ConnectionError("No connection")
|
mock_get.side_effect = ConnectionError("No connection")
|
||||||
with patch("utils.pogoda.fetch_open_meteo") as mock_fallback:
|
with patch("utils.pogoda.fetch_open_meteo") as mock_fallback:
|
||||||
mock_fallback.return_value = {"result": "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"}
|
assert result == {"result": "fallback"}
|
||||||
|
|
||||||
@patch("utils.pogoda._session.get")
|
@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."""
|
"""Timeout → fallback на Open-Meteo."""
|
||||||
from requests.exceptions import Timeout
|
from requests.exceptions import Timeout
|
||||||
mock_get.side_effect = Timeout("Timed out")
|
mock_get.side_effect = Timeout("Timed out")
|
||||||
with patch("utils.pogoda.fetch_open_meteo") as mock_fallback:
|
with patch("utils.pogoda.fetch_open_meteo") as mock_fallback:
|
||||||
mock_fallback.return_value = {"result": "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"}
|
assert result == {"result": "fallback"}
|
||||||
|
|
||||||
@patch("utils.pogoda._session.get")
|
@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."""
|
"""Все попытки не удались → fallback на Open-Meteo."""
|
||||||
from requests.exceptions import ConnectionError
|
from requests.exceptions import ConnectionError
|
||||||
mock_get.side_effect = ConnectionError("No connection")
|
mock_get.side_effect = ConnectionError("No connection")
|
||||||
with patch("utils.pogoda.fetch_open_meteo") as mock_fallback:
|
with patch("utils.pogoda.fetch_open_meteo") as mock_fallback:
|
||||||
mock_fallback.return_value = None
|
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
|
assert result is None
|
||||||
|
|
||||||
@patch("utils.pogoda._session.get")
|
@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."""
|
"""Общий RequestException → fallback на Open-Meteo."""
|
||||||
import requests
|
import requests
|
||||||
mock_get.side_effect = requests.RequestException("Generic error")
|
mock_get.side_effect = requests.RequestException("Generic error")
|
||||||
with patch("utils.pogoda.fetch_open_meteo") as mock_fallback:
|
with patch("utils.pogoda.fetch_open_meteo") as mock_fallback:
|
||||||
mock_fallback.return_value = {"result": "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"}
|
assert result == {"result": "fallback"}
|
||||||
|
|
||||||
@patch("utils.pogoda._session.get")
|
@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) не ловится, падает."""
|
"""HTTP-ошибка (raise_for_status) не ловится, падает."""
|
||||||
mock_response = MagicMock()
|
mock_response = MagicMock()
|
||||||
mock_response.raise_for_status.side_effect = Exception("HTTP 500")
|
mock_response.raise_for_status.side_effect = Exception("HTTP 500")
|
||||||
mock_get.return_value = mock_response
|
mock_get.return_value = mock_response
|
||||||
with pytest.raises(Exception):
|
with pytest.raises(Exception):
|
||||||
asyncio.run(fetch_weather("https://test.example.com"))
|
await fetch_weather("https://test.example.com")
|
||||||
|
|
||||||
|
|
||||||
class TestFetchOpenMeteo:
|
class TestFetchOpenMeteo:
|
||||||
"""Тесты функции fetch_open_meteo() — fallback на Open-Meteo API."""
|
"""Тесты функции fetch_open_meteo() — fallback на Open-Meteo API."""
|
||||||
|
|
||||||
@patch("utils.pogoda._session.get")
|
@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."""
|
"""Успешный ответ должен вернуть данные в формате current_condition."""
|
||||||
mock_response = MagicMock()
|
mock_response = MagicMock()
|
||||||
mock_response.json.return_value = {
|
mock_response.json.return_value = {
|
||||||
@ -99,7 +99,7 @@ class TestFetchOpenMeteo:
|
|||||||
}
|
}
|
||||||
mock_response.raise_for_status = MagicMock()
|
mock_response.raise_for_status = MagicMock()
|
||||||
mock_get.return_value = mock_response
|
mock_get.return_value = mock_response
|
||||||
result = asyncio.run(fetch_open_meteo())
|
result = await fetch_open_meteo()
|
||||||
assert result is not None
|
assert result is not None
|
||||||
assert "current_condition" in result
|
assert "current_condition" in result
|
||||||
assert result["current_condition"][0]["temp_C"] == 15
|
assert result["current_condition"][0]["temp_C"] == 15
|
||||||
@ -108,13 +108,13 @@ class TestFetchOpenMeteo:
|
|||||||
assert result["current_condition"][0]["pressure"] == 1013
|
assert result["current_condition"][0]["pressure"] == 1013
|
||||||
|
|
||||||
@patch("utils.pogoda._session.get")
|
@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."""
|
"""Кастомные координаты должны быть в URL."""
|
||||||
mock_response = MagicMock()
|
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.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_response.raise_for_status = MagicMock()
|
||||||
mock_get.return_value = mock_response
|
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
|
assert result is not None
|
||||||
mock_get.assert_called_once()
|
mock_get.assert_called_once()
|
||||||
call_url = mock_get.call_args[0][0]
|
call_url = mock_get.call_args[0][0]
|
||||||
@ -122,95 +122,95 @@ class TestFetchOpenMeteo:
|
|||||||
assert "37.6173" in call_url
|
assert "37.6173" in call_url
|
||||||
|
|
||||||
@patch("utils.pogoda._session.get")
|
@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 → 'Неизвестно'."""
|
"""Отсутствующий weather_code → 'Неизвестно'."""
|
||||||
mock_response = MagicMock()
|
mock_response = MagicMock()
|
||||||
mock_response.json.return_value = {"current": {"temperature": 10}}
|
mock_response.json.return_value = {"current": {"temperature": 10}}
|
||||||
mock_response.raise_for_status = MagicMock()
|
mock_response.raise_for_status = MagicMock()
|
||||||
mock_get.return_value = mock_response
|
mock_get.return_value = mock_response
|
||||||
result = asyncio.run(fetch_open_meteo())
|
result = await fetch_open_meteo()
|
||||||
assert result is not None
|
assert result is not None
|
||||||
assert result["current_condition"][0]["weatherDesc"] == [{"value": "Неизвестно"}]
|
assert result["current_condition"][0]["weatherDesc"] == [{"value": "Неизвестно"}]
|
||||||
|
|
||||||
@patch("utils.pogoda._session.get")
|
@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."""
|
"""SSLError → вернуть None."""
|
||||||
from requests.exceptions import SSLError
|
from requests.exceptions import SSLError
|
||||||
mock_get.side_effect = SSLError("SSL Error")
|
mock_get.side_effect = SSLError("SSL Error")
|
||||||
with patch("utils.pogoda.fetch_open_meteo") as mock_fallback:
|
with patch("utils.pogoda.fetch_open_meteo") as mock_fallback:
|
||||||
# Внутренний fallback тоже падает, проверяем что возвращается None
|
# Внутренний fallback тоже падает, проверяем что возвращается None
|
||||||
pass
|
pass
|
||||||
result = asyncio.run(fetch_open_meteo())
|
result = await fetch_open_meteo()
|
||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
@patch("utils.pogoda._session.get")
|
@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."""
|
"""ConnectionError → вернуть None."""
|
||||||
from requests.exceptions import ConnectionError
|
from requests.exceptions import ConnectionError
|
||||||
mock_get.side_effect = ConnectionError("No connection")
|
mock_get.side_effect = ConnectionError("No connection")
|
||||||
result = asyncio.run(fetch_open_meteo())
|
result = await fetch_open_meteo()
|
||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
@patch("utils.pogoda._session.get")
|
@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."""
|
"""Timeout → вернуть None."""
|
||||||
from requests.exceptions import Timeout
|
from requests.exceptions import Timeout
|
||||||
mock_get.side_effect = Timeout("Timed out")
|
mock_get.side_effect = Timeout("Timed out")
|
||||||
result = asyncio.run(fetch_open_meteo())
|
result = await fetch_open_meteo()
|
||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
@patch("utils.pogoda._session.get")
|
@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."""
|
"""Общий RequestException → вернуть None."""
|
||||||
import requests
|
import requests
|
||||||
mock_get.side_effect = requests.RequestException("Error")
|
mock_get.side_effect = requests.RequestException("Error")
|
||||||
result = asyncio.run(fetch_open_meteo())
|
result = await fetch_open_meteo()
|
||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
@patch("utils.pogoda._session.get")
|
@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."""
|
"""Ошибка парсинга JSON → вернуть None."""
|
||||||
import requests
|
import requests
|
||||||
mock_response = MagicMock()
|
mock_response = MagicMock()
|
||||||
mock_response.json.side_effect = requests.RequestException("JSON Error")
|
mock_response.json.side_effect = requests.RequestException("JSON Error")
|
||||||
mock_response.raise_for_status = MagicMock()
|
mock_response.raise_for_status = MagicMock()
|
||||||
mock_get.return_value = mock_response
|
mock_get.return_value = mock_response
|
||||||
result = asyncio.run(fetch_open_meteo())
|
result = await fetch_open_meteo()
|
||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
@patch("utils.pogoda._session.get")
|
@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: первая попытка падает, вторая успешна."""
|
"""Retry: первая попытка падает, вторая успешна."""
|
||||||
from requests.exceptions import ConnectionError
|
from requests.exceptions import ConnectionError
|
||||||
success_response = MagicMock()
|
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.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()
|
success_response.raise_for_status = MagicMock()
|
||||||
mock_get.side_effect = [ConnectionError("fail"), success_response]
|
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 result is not None
|
||||||
assert mock_get.call_count == 2
|
assert mock_get.call_count == 2
|
||||||
|
|
||||||
@patch("utils.pogoda._session.get")
|
@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."""
|
"""Все попытки неудачны → None."""
|
||||||
from requests.exceptions import ConnectionError
|
from requests.exceptions import ConnectionError
|
||||||
mock_get.side_effect = [ConnectionError("fail"), ConnectionError("fail"), ConnectionError("fail")]
|
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 result is None
|
||||||
assert mock_get.call_count == 3
|
assert mock_get.call_count == 3
|
||||||
|
|
||||||
@patch("utils.pogoda._session.get")
|
@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."""
|
"""HTTP 404 → raise_for_status бросит исключение → None."""
|
||||||
import requests
|
import requests
|
||||||
mock_response = MagicMock()
|
mock_response = MagicMock()
|
||||||
mock_response.raise_for_status.side_effect = requests.HTTPError("HTTP 404")
|
mock_response.raise_for_status.side_effect = requests.HTTPError("HTTP 404")
|
||||||
mock_get.return_value = mock_response
|
mock_get.return_value = mock_response
|
||||||
result = asyncio.run(fetch_open_meteo())
|
result = await fetch_open_meteo()
|
||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
@patch("utils.pogoda._session.get")
|
@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 = MagicMock()
|
||||||
mock_response.json.return_value = {
|
mock_response.json.return_value = {
|
||||||
@ -225,7 +225,7 @@ class TestFetchOpenMeteo:
|
|||||||
}
|
}
|
||||||
mock_response.raise_for_status = MagicMock()
|
mock_response.raise_for_status = MagicMock()
|
||||||
mock_get.return_value = mock_response
|
mock_get.return_value = mock_response
|
||||||
result = asyncio.run(fetch_open_meteo())
|
result = await fetch_open_meteo()
|
||||||
assert result is not None
|
assert result is not None
|
||||||
assert result["current_condition"][0]["windspeedKmph"] == 0
|
assert result["current_condition"][0]["windspeedKmph"] == 0
|
||||||
assert result["current_condition"][0]["pressure"] == 1000
|
assert result["current_condition"][0]["pressure"] == 1000
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user