import pytest from utils.news import ( format_articles, truncate_embed_field, truncate_embed_text, truncate_message, truncate_title, _parse_date, ) class TestTruncateTitle: """Тесты функции truncate_title() — обрезка заголовка.""" @pytest.mark.parametrize( "title, max_len, expected", [ ("Короткий заголовок", 60, "Короткий заголовок"), ( "Заголовок ровно в 60 символов1234567890", 60, "Заголовок ровно в 60 символов1234567890", ), ("A" * 80, 60, "A" * 60 + "..."), # ASCII для надёжного сравнения ("", 60, ""), ("A" * 100, 100, "A" * 100), ("A" * 101, 100, "A" * 100 + "..."), ("A" * 50, 100, "A" * 50), ], ) def test_truncate(self, title, max_len, expected) -> None: """Проверка обрезки заголовка.""" assert truncate_title(title, max_len) == expected def test_truncate_default_max_len(self) -> None: """По умолчанию max_len=60.""" long_title = "A" * 61 result = truncate_title(long_title) assert result == "A" * 60 + "..." assert len(result) == 63 # 60 + "..." class TestParseDate: """Тесты функции _parse_date() — парсинг даты из RSS.""" @pytest.mark.parametrize( "pub_date, expected", [ ("Mon, 28 May 2026 10:00:00 +0000", "28.05.2026"), ("Mon, 28 May 2026 10:00:00 GMT", "28.05.2026"), ("2026-05-28T10:00:00Z", "28.05.2026"), ("2026-12-31T23:59:59Z", "31.12.2026"), ("2026-01-01T00:00:00Z", "01.01.2026"), ("2026-05-28", "28.05.2026"), ], ) def test_parse_date_known(self, pub_date, expected) -> None: """Известные форматы даты должны парситься корректно.""" assert _parse_date(pub_date) == expected @pytest.mark.parametrize( "pub_date, expected", [ ("", ""), (None, ""), ], ) def test_parse_date_empty(self, pub_date, expected) -> None: """Пустая или None дата должна вернуть пустую строку.""" assert _parse_date(pub_date) == expected def test_parse_date_invalid(self) -> None: """Невалидная дата должна вернуть пустую строку.""" result = _parse_date("invalid-date-string") assert result == "" class TestFormatArticles: """Тесты функции format_articles() — формирование строк для вывода.""" def test_format_articles_normal(self) -> None: """Нормальный список статей должен вернуть заголовок + 5 статей.""" articles = [ { "title": "Статья 1", "link": "https://habr.com/1", "pub_date": "Mon, 28 May 2026 10:00:00 +0000", "creator": "Автор 1", "tags": ["AI"], }, { "title": "Статья 2", "link": "https://habr.com/2", "pub_date": "Tue, 29 May 2026 12:00:00 +0000", "creator": "Автор 2", "tags": ["ML"], }, ] result = format_articles(articles, "Заголовок", "https://habr.com/feed") assert len(result) == 3 # заголовок + 2 статьи assert result[0] == "Заголовок\n" assert result[1] == "Статья 1\n28.05.2026 " assert result[2] == "Статья 2\n29.05.2026 " def test_format_articles_limit_to_5(self) -> None: """Больше 5 статей должно быть обрезано до 5.""" articles = [ { "title": f"Статья {i}", "link": f"https://habr.com/{i}", "pub_date": "Mon, 28 May 2026 10:00:00 +0000", "creator": "", "tags": [], } for i in range(10) ] result = format_articles(articles, "Заголовок", "https://habr.com/feed") assert len(result) == 6 # заголовок + 5 статей assert result[-1] == "Статья 4\n28.05.2026 " def test_format_articles_empty_list(self) -> None: """Пустой список должен вернуть только заголовок.""" result = format_articles([], "Заголовок", "https://habr.com/feed") assert result == ["Заголовок\n"] assert len(result) == 1 def test_format_articles_none(self) -> None: """None должен вызвать TypeError (articles[:5] на None).""" with pytest.raises(TypeError): format_articles(None, "Заголовок", "https://habr.com/feed") def test_format_articles_single_article(self) -> None: """Одна статья должна быть корректно отформатирована.""" articles = [ { "title": "Единственная статья", "link": "https://habr.com/1", "pub_date": "Mon, 28 May 2026 10:00:00 +0000", "creator": "Автор", "tags": ["AI"], }, ] result = format_articles(articles, "Новости AI", "https://habr.com/ai") assert len(result) == 2 assert result[0] == "Новости AI\n" assert result[1] == "Единственная статья\n28.05.2026 " def test_format_articles_long_title_truncated(self) -> None: """Длинный заголовок должен быть обрезан до 60 символов с '...'.""" long_title = "A" * 100 articles = [ { "title": long_title, "link": "https://habr.com/1", "pub_date": "Mon, 28 May 2026 10:00:00 +0000", "creator": "", "tags": [], } ] result = format_articles(articles, "Заголовок", "https://habr.com/feed") assert len(result[1].split("\n")[0]) == 63 # 60 + "..." assert result[1].split("\n")[0].endswith("...") def test_format_articles_short_title_unchanged(self) -> None: """Короткий заголовок должен остаться без изменений.""" short_title = "Кот" articles = [ { "title": short_title, "link": "https://habr.com/1", "pub_date": "Mon, 28 May 2026 10:00:00 +0000", "creator": "", "tags": [], } ] result = format_articles(articles, "Заголовок", "https://habr.com/feed") assert result[1].split("\n")[0] == "Кот" def test_format_articles_exact_60_chars(self) -> None: """Заголовок ровно 60 символов не должен обрезаться.""" exact_title = "A" * 60 articles = [ { "title": exact_title, "link": "https://habr.com/1", "pub_date": "Mon, 28 May 2026 10:00:00 +0000", "creator": "", "tags": [], } ] result = format_articles(articles, "Заголовок", "https://habr.com/feed") assert result[1].split("\n")[0] == exact_title assert "..." not in result[1] def test_format_articles_iso_date(self) -> None: """Дата в формате ISO должна парситься корректно.""" articles = [ { "title": "Статья", "link": "https://habr.com/1", "pub_date": "2026-05-28T10:00:00Z", "creator": "", "tags": [], }, ] result = format_articles(articles, "Заголовок", "https://habr.com/feed") assert result[1] == "Статья\n28.05.2026 " def test_format_articles_empty_date(self) -> None: """Пустая дата должна быть пустой строкой.""" articles = [ { "title": "Статья", "link": "https://habr.com/1", "pub_date": "", "creator": "", "tags": [], } ] result = format_articles(articles, "Заголовок", "https://habr.com/feed") assert result[1] == "Статья\n " def test_format_articles_none_date(self) -> None: """None дата должна быть пустой строкой.""" articles = [ { "title": "Статья", "link": "https://habr.com/1", "pub_date": None, "creator": "", "tags": [], } ] result = format_articles(articles, "Заголовок", "https://habr.com/feed") assert result[1] == "Статья\n " def test_format_articles_empty_link(self) -> None: """Пустая ссылка должна быть пустой строкой в угловых скобках.""" articles = [ { "title": "Статья", "link": "", "pub_date": "Mon, 28 May 2026 10:00:00 +0000", "creator": "", "tags": [], } ] result = format_articles(articles, "Заголовок", "https://habr.com/feed") assert result[1].endswith(" <>") def test_format_articles_russian_title(self) -> None: """Русские заголовки должны корректно отображаться.""" articles = [ { "title": "Искусственный интеллект в медицине", "link": "https://habr.com/1", "pub_date": "Mon, 28 May 2026 10:00:00 +0000", "creator": "Иван Иванов", "tags": ["AI", "медицина"], }, ] result = format_articles(articles, "Новости AI", "https://habr.com/ai") assert "Искусственный интеллект в медицине" in result[1] def test_format_articles_exact_5_articles(self) -> None: """Ровно 5 статей должно быть включено.""" articles = [ { "title": f"Статья {i}", "link": f"https://habr.com/{i}", "pub_date": "Mon, 28 May 2026 10:00:00 +0000", "creator": "", "tags": [], } for i in range(5) ] result = format_articles(articles, "Заголовок", "https://habr.com/feed") assert len(result) == 6 # заголовок + 5 статей assert result[-1] == "Статья 4\n28.05.2026 " def test_format_articles_6th_article_excluded(self) -> None: """6-я статья должна быть исключена.""" articles = [ { "title": f"Статья {i}", "link": f"https://habr.com/{i}", "pub_date": "Mon, 28 May 2026 10:00:00 +0000", "creator": "", "tags": [], } for i in range(6) ] result = format_articles(articles, "Заголовок", "https://habr.com/feed") assert len(result) == 6 # заголовок + 5 статей assert "Статья 5" not in result[5] class TestTruncateMessage: """Тесты функции truncate_message() — обрезка plain text сообщений.""" def test_short_message_unchanged(self) -> None: """Короткое сообщение не обрезается.""" text = "Короткое сообщение" assert truncate_message(text) == text def test_exact_limit_unchanged(self) -> None: """Текст ровно 2000 символов не обрезается.""" text = "A" * 2000 assert truncate_message(text) == text def test_over_limit_truncated(self) -> None: """Текст больше 2000 символов обрезается.""" text = "A" * 2500 result = truncate_message(text) assert len(result) == 2000 assert result.endswith("...") def test_custom_max_len(self) -> None: """Кастомный max_len.""" text = "A" * 150 result = truncate_message(text, max_len=100) assert len(result) == 100 assert result.endswith("...") def test_under_custom_max_len(self) -> None: """Текст меньше кастомного max_len не обрезается.""" text = "A" * 50 result = truncate_message(text, max_len=100) assert result == text class TestTruncateEmbedText: """Тесты функции truncate_embed_text() — обрезка embed.description.""" def test_short_text_unchanged(self) -> None: """Короткий текст не обрезается.""" text = "Short" assert truncate_embed_text(text) == text def test_exact_limit_unchanged(self) -> None: """Текст ровно 4096 символов не обрезается.""" text = "A" * 4096 assert truncate_embed_text(text) == text def test_over_limit_truncated(self) -> None: """Текст больше 4096 символов обрезается.""" text = "A" * 5000 result = truncate_embed_text(text) assert len(result) == 4096 assert result.endswith("...") def test_custom_max_len(self) -> None: """Кастомный max_len.""" text = "A" * 200 result = truncate_embed_text(text, max_len=100) assert len(result) == 100 assert result.endswith("...") class TestTruncateEmbedField: """Тесты функции truncate_embed_field() — обрезка embed field value.""" def test_short_text_unchanged(self) -> None: """Короткий текст не обрезается.""" text = "Short" assert truncate_embed_field(text) == text def test_exact_limit_unchanged(self) -> None: """Текст ровно 1024 символа не обрезается.""" text = "A" * 1024 assert truncate_embed_field(text) == text def test_over_limit_truncated(self) -> None: """Текст больше 1024 символов обрезается.""" text = "A" * 2000 result = truncate_embed_field(text) assert len(result) == 1024 assert result.endswith("...") def test_custom_max_len(self) -> None: """Кастомный max_len.""" text = "A" * 150 result = truncate_embed_field(text, max_len=100) assert len(result) == 100 assert result.endswith("...")