- pytest.ini для конфигурации тестов - tests/test_pogoda.py — тесты translate_weather, pressure_to_mmhg, wmo_to_russian (93 теста) - tests/test_fetch_cat.py — тесты fetch_cat (10 тестов) - tests/test_fetch_rss.py — тесты fetch_rss (20 тестов) - tests/test_format_articles.py — тесты truncate_title, parse_date, format_articles (24 теста) - tests/test_fetch_weather.py — тесты fetch_weather, fetch_open_meteo (20 тестов) - tests/test_commands_pogoda.py — тесты команды !pogoda (13 тестов) - Обновить AGENTS.md и requirements.txt
417 lines
17 KiB
Python
417 lines
17 KiB
Python
import asyncio
|
||
import pytest
|
||
from unittest.mock import patch, MagicMock
|
||
from utils.news import fetch_rss
|
||
|
||
|
||
class TestFetchRss:
|
||
"""Тесты функции fetch_rss() — получение и парсинг RSS-ленты."""
|
||
|
||
@patch("utils.news._session.get")
|
||
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">
|
||
<channel>
|
||
<item>
|
||
<title>Статья 1</title>
|
||
<link>https://habr.com/1</link>
|
||
<guid isPermaLink="true">https://habr.com/1</guid>
|
||
<pubDate>Mon, 28 May 2026 10:00:00 +0000</pubDate>
|
||
<dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Автор 1</dc:creator>
|
||
<category>AI</category>
|
||
<category>ML</category>
|
||
</item>
|
||
<item>
|
||
<title>Статья 2</title>
|
||
<guid isPermaLink="true">https://habr.com/2</guid>
|
||
<pubDate>Mon, 28 May 2026 12:00:00 +0000</pubDate>
|
||
<dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Автор 2</dc:creator>
|
||
</item>
|
||
</channel>
|
||
</rss>""".encode()
|
||
mock_response = MagicMock()
|
||
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"))
|
||
assert result is not None
|
||
assert len(result) == 2
|
||
assert result[0]["title"] == "Статья 1"
|
||
assert result[0]["link"] == "https://habr.com/1"
|
||
assert result[0]["pub_date"] == "Mon, 28 May 2026 10:00:00 +0000"
|
||
assert result[0]["creator"] == "Автор 1"
|
||
assert result[0]["tags"] == ["AI", "ML"]
|
||
assert result[1]["title"] == "Статья 2"
|
||
assert result[1]["creator"] == "Автор 2"
|
||
assert result[1]["tags"] == []
|
||
|
||
@patch("utils.news._session.get")
|
||
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">
|
||
<entry>
|
||
<title>Atom статья 1</title>
|
||
<link href="https://habr.com/atom/1" />
|
||
<published>2026-05-28T10:00:00Z</published>
|
||
<author><name>Atom автор</name></author>
|
||
<category>AI</category>
|
||
</entry>
|
||
</feed>""".encode()
|
||
mock_response = MagicMock()
|
||
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"))
|
||
assert result is not None
|
||
assert len(result) == 1
|
||
assert result[0]["title"] == "Atom статья 1"
|
||
assert result[0]["link"] == "https://habr.com/atom/1"
|
||
assert result[0]["pub_date"] == "2026-05-28T10:00:00Z"
|
||
assert result[0]["creator"] == "Atom автор"
|
||
assert result[0]["tags"] == ["AI"]
|
||
|
||
@patch("utils.news._session.get")
|
||
def test_fetch_rss_empty_items(self, mock_get):
|
||
"""RSS без items должен вернуть пустой список."""
|
||
rss_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||
<rss version="2.0">
|
||
<channel>
|
||
</channel>
|
||
</rss>""".encode()
|
||
mock_response = MagicMock()
|
||
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"))
|
||
assert result == []
|
||
|
||
@patch("utils.news._session.get")
|
||
def test_fetch_rss_no_matching_format(self, mock_get):
|
||
"""Неизвестный формат XML должен вернуть пустой список."""
|
||
xml_content = """<?xml version="1.0"?>
|
||
<unknown></unknown>""".encode()
|
||
mock_response = MagicMock()
|
||
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"))
|
||
assert result == []
|
||
|
||
@patch("utils.news._session.get")
|
||
def test_fetch_rss_missing_title(self, mock_get):
|
||
"""Статья без title должна получить 'Без названия'."""
|
||
rss_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||
<rss version="2.0">
|
||
<channel>
|
||
<item>
|
||
<title>Без title</title>
|
||
<guid isPermaLink="true">https://habr.com/1</guid>
|
||
<pubDate>Mon, 28 May 2026 10:00:00 +0000</pubDate>
|
||
</item>
|
||
</channel>
|
||
</rss>""".encode()
|
||
mock_response = MagicMock()
|
||
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"))
|
||
assert result is not None
|
||
assert result[0]["title"] == "Без title"
|
||
assert result[0]["link"] == "https://habr.com/1"
|
||
assert result[0]["pub_date"] == "Mon, 28 May 2026 10:00:00 +0000"
|
||
assert result[0]["creator"] == ""
|
||
assert result[0]["tags"] == []
|
||
|
||
@patch("utils.news._session.get")
|
||
def test_fetch_rss_missing_guid(self, mock_get):
|
||
"""Статья без guid isPermaLink должна иметь пустую ссылку."""
|
||
rss_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||
<rss version="2.0">
|
||
<channel>
|
||
<item>
|
||
<title>Без guid</title>
|
||
<link>https://habr.com/1</link>
|
||
<guid>https://habr.com/1</guid>
|
||
</item>
|
||
</channel>
|
||
</rss>""".encode()
|
||
mock_response = MagicMock()
|
||
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"))
|
||
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):
|
||
"""Больше 10 items должно быть обрезано до 10."""
|
||
items = "\n".join(
|
||
f""" <item>
|
||
<title>Статья {i}</title>
|
||
<guid isPermaLink="true">https://habr.com/{i}</guid>
|
||
<pubDate>Mon, 28 May 2026 10:00:00 +0000</pubDate>
|
||
</item>"""
|
||
for i in range(15)
|
||
)
|
||
rss_content = (f"""<?xml version="1.0" encoding="UTF-8"?>
|
||
<rss version="2.0">
|
||
<channel>
|
||
{items}
|
||
</channel>
|
||
</rss>""").encode()
|
||
mock_response = MagicMock()
|
||
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"))
|
||
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):
|
||
"""HTTP-ошибка должна вернуть None."""
|
||
import requests
|
||
mock_get.side_effect = requests.exceptions.HTTPError("404 Not Found")
|
||
result = asyncio.run(fetch_rss("https://example.com/rss"))
|
||
assert result is None
|
||
|
||
@patch("utils.news._session.get")
|
||
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"))
|
||
assert result is None
|
||
|
||
@patch("utils.news._session.get")
|
||
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"))
|
||
assert result is None
|
||
|
||
@patch("utils.news._session.get")
|
||
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"))
|
||
assert result is None
|
||
|
||
@patch("utils.news._session.get")
|
||
def test_fetch_rss_empty_tags(self, mock_get):
|
||
"""Статья с пустыми тегами должна иметь пустые строки."""
|
||
rss_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||
<rss version="2.0">
|
||
<channel>
|
||
<item>
|
||
<title>Пустые теги</title>
|
||
<guid isPermaLink="true">https://habr.com/1</guid>
|
||
<pubDate>Mon, 28 May 2026 10:00:00 +0000</pubDate>
|
||
</item>
|
||
</channel>
|
||
</rss>""".encode()
|
||
mock_response = MagicMock()
|
||
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"))
|
||
assert result is not None
|
||
assert result[0]["title"] == "Пустые теги"
|
||
assert result[0]["link"] == "https://habr.com/1"
|
||
assert result[0]["pub_date"] == "Mon, 28 May 2026 10:00:00 +0000"
|
||
assert result[0]["creator"] == ""
|
||
assert result[0]["tags"] == []
|
||
|
||
@patch("utils.news._session.get")
|
||
def test_fetch_rss_category_without_text(self, mock_get):
|
||
"""Категория без текста должна быть пропущена."""
|
||
rss_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||
<rss version="2.0">
|
||
<channel>
|
||
<item>
|
||
<title>Статья</title>
|
||
<guid isPermaLink="true">https://habr.com/1</guid>
|
||
<category></category>
|
||
<category>AI</category>
|
||
</item>
|
||
</channel>
|
||
</rss>""".encode()
|
||
mock_response = MagicMock()
|
||
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"))
|
||
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):
|
||
"""Atom feed без автора должен иметь пустого creator."""
|
||
atom_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||
<entry>
|
||
<title>Без автора</title>
|
||
<link href="https://habr.com/1" />
|
||
<published>2026-05-28T10:00:00Z</published>
|
||
</entry>
|
||
</feed>""".encode()
|
||
mock_response = MagicMock()
|
||
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"))
|
||
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):
|
||
"""Atom feed без link должен иметь пустую ссылку."""
|
||
atom_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||
<entry>
|
||
<title>Без ссылки</title>
|
||
<published>2026-05-28T10:00:00Z</published>
|
||
</entry>
|
||
</feed>""".encode()
|
||
mock_response = MagicMock()
|
||
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"))
|
||
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):
|
||
"""Общий RequestException должен вернуть None."""
|
||
import requests
|
||
mock_get.side_effect = requests.RequestException("Generic error")
|
||
result = asyncio.run(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):
|
||
"""Если нет guid isPermaLink, ссылка должна быть пустой."""
|
||
rss_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||
<rss version="2.0">
|
||
<channel>
|
||
<item>
|
||
<title>Статья</title>
|
||
<link>https://habr.com/alternative</link>
|
||
<guid>https://habr.com/alternative</guid>
|
||
</item>
|
||
</channel>
|
||
</rss>""".encode()
|
||
mock_response = MagicMock()
|
||
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"))
|
||
assert result is not None
|
||
assert result[0]["link"] == ""
|
||
|
||
@patch("utils.news._session.get")
|
||
def test_fetch_rss_single_item(self, mock_get):
|
||
"""Один item должен быть распарсен корректно."""
|
||
rss_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||
<rss version="2.0">
|
||
<channel>
|
||
<item>
|
||
<title>Единственная статья</title>
|
||
<guid isPermaLink="true">https://habr.com/1</guid>
|
||
<pubDate>Mon, 28 May 2026 10:00:00 +0000</pubDate>
|
||
<dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Единственный автор</dc:creator>
|
||
<category>ML</category>
|
||
</item>
|
||
</channel>
|
||
</rss>""".encode()
|
||
mock_response = MagicMock()
|
||
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"))
|
||
assert result is not None
|
||
assert len(result) == 1
|
||
assert result[0]["title"] == "Единственная статья"
|
||
assert result[0]["link"] == "https://habr.com/1"
|
||
assert result[0]["creator"] == "Единственный автор"
|
||
assert result[0]["tags"] == ["ML"]
|
||
|
||
@patch("utils.news._session.get")
|
||
def test_fetch_rss_special_characters_in_title(self, mock_get):
|
||
"""Заголовки со спецсимволами должны парситься корректно."""
|
||
rss_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||
<rss version="2.0">
|
||
<channel>
|
||
<item>
|
||
<title>AI & ML: будущее <технологий></title>
|
||
<guid isPermaLink="true">https://habr.com/1</guid>
|
||
<pubDate>Mon, 28 May 2026 10:00:00 +0000</pubDate>
|
||
</item>
|
||
</channel>
|
||
</rss>""".encode()
|
||
mock_response = MagicMock()
|
||
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"))
|
||
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):
|
||
"""Дата с GMT должна парситься корректно."""
|
||
rss_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||
<rss version="2.0">
|
||
<channel>
|
||
<item>
|
||
<title>Статья</title>
|
||
<guid isPermaLink="true">https://habr.com/1</guid>
|
||
<pubDate>Mon, 28 May 2026 10:00:00 GMT</pubDate>
|
||
</item>
|
||
</channel>
|
||
</rss>""".encode()
|
||
mock_response = MagicMock()
|
||
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"))
|
||
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):
|
||
"""Множество категорий должны быть собраны."""
|
||
rss_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||
<rss version="2.0">
|
||
<channel>
|
||
<item>
|
||
<title>Статья</title>
|
||
<guid isPermaLink="true">https://habr.com/1</guid>
|
||
<category>AI</category>
|
||
<category>ML</category>
|
||
<category>Deep Learning</category>
|
||
<category>NLP</category>
|
||
<category>Computer Vision</category>
|
||
</item>
|
||
</channel>
|
||
</rss>""".encode()
|
||
mock_response = MagicMock()
|
||
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"))
|
||
assert result is not None
|
||
assert result[0]["tags"] == ["AI", "ML", "Deep Learning", "NLP", "Computer Vision"]
|