discordBot/tests/test_fetch_rss.py
deadzilla 4b9bb7e97a Добавить pytest-тесты и конфигурацию
- 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
2026-05-29 15:45:56 +05:00

417 lines
17 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 &amp; ML: будущее &lt;технологий&gt;</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"]