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 = """ Статья 1 https://habr.com/1 https://habr.com/1 Mon, 28 May 2026 10:00:00 +0000 Автор 1 AI ML Статья 2 https://habr.com/2 Mon, 28 May 2026 12:00:00 +0000 Автор 2 """.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 = """ Atom статья 1 2026-05-28T10:00:00Z Atom автор AI """.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 = """ """.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 = """ """.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 = """ Без title https://habr.com/1 Mon, 28 May 2026 10:00:00 +0000 """.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 = """ Без guid https://habr.com/1 https://habr.com/1 """.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""" Статья {i} https://habr.com/{i} Mon, 28 May 2026 10:00:00 +0000 """ for i in range(15) ) rss_content = (f""" {items} """).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 = """ Пустые теги https://habr.com/1 Mon, 28 May 2026 10:00:00 +0000 """.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 = """ Статья https://habr.com/1 AI """.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 = """ Без автора 2026-05-28T10:00:00Z """.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 = """ Без ссылки 2026-05-28T10:00:00Z """.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 = """ Статья https://habr.com/alternative https://habr.com/alternative """.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 = """ Единственная статья https://habr.com/1 Mon, 28 May 2026 10:00:00 +0000 Единственный автор ML """.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 = """ AI & ML: будущее <технологий> https://habr.com/1 Mon, 28 May 2026 10:00:00 +0000 """.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 = """ Статья https://habr.com/1 Mon, 28 May 2026 10:00:00 GMT """.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 = """ Статья https://habr.com/1 AI ML Deep Learning NLP Computer Vision """.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"]