386 lines
15 KiB
Python
386 lines
15 KiB
Python
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<https://habr.com/feed>"
|
||
assert result[1] == "Статья 1\n28.05.2026 <https://habr.com/1>"
|
||
assert result[2] == "Статья 2\n29.05.2026 <https://habr.com/2>"
|
||
|
||
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 <https://habr.com/4>"
|
||
|
||
def test_format_articles_empty_list(self) -> None:
|
||
"""Пустой список должен вернуть только заголовок."""
|
||
result = format_articles([], "Заголовок", "https://habr.com/feed")
|
||
assert result == ["Заголовок\n<https://habr.com/feed>"]
|
||
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<https://habr.com/ai>"
|
||
assert result[1] == "Единственная статья\n28.05.2026 <https://habr.com/1>"
|
||
|
||
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 <https://habr.com/1>"
|
||
|
||
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 <https://habr.com/1>"
|
||
|
||
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 <https://habr.com/1>"
|
||
|
||
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 <https://habr.com/4>"
|
||
|
||
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("...")
|