"""Extended FeedManager tests: sort, format_message, mocked RSS/API fetch, queue processing."""
from __future__ import annotations
import json
from configparser import ConfigParser
from datetime import datetime, timezone
from unittest.mock import AsyncMock, MagicMock, Mock, patch
import pytest
from modules.db_manager import DBManager
from modules.feed_manager import FeedManager
def _feed_manager_bot(mock_logger, db_path: str):
bot = Mock()
bot.logger = mock_logger
bot.config = ConfigParser()
bot.config.add_section("Feed_Manager")
bot.config.set("Feed_Manager", "feed_manager_enabled", "false")
bot.config.set("Feed_Manager", "max_message_length", "200")
bot.db_manager = DBManager(bot, db_path)
return bot
@pytest.fixture
def fm_with_db(mock_logger, tmp_path):
"""FeedManager backed by a real file SQLite DB (feed tables from DBManager)."""
db_path = str(tmp_path / "feeds.db")
bot = _feed_manager_bot(mock_logger, db_path)
return FeedManager(bot)
def _seed_feed_subscription(db_manager: DBManager, feed_id: int = 1, channel_name: str = "general") -> None:
"""Insert a minimal feed_subscriptions row so feed_activity / feed_errors FK inserts succeed."""
with db_manager.connection() as conn:
conn.execute(
"""
INSERT OR IGNORE INTO feed_subscriptions
(id, feed_type, feed_url, channel_name, enabled)
VALUES (?, 'rss', 'http://example.com/feed.xml', ?, 1)
""",
(feed_id, channel_name),
)
conn.commit()
def _fake_aiohttp_response(*, text_body: str | None = None, json_body: dict | list | None = None):
"""Build a minimal async context manager compatible with async with session.get/post."""
resp = Mock()
resp.status = 200
if text_body is not None:
resp.text = AsyncMock(return_value=text_body)
if json_body is not None:
resp.json = AsyncMock(return_value=json_body)
class _CM:
def __init__(self, r):
self._r = r
async def __aenter__(self):
return self._r
async def __aexit__(self, exc_type, exc, tb):
return False
return _CM(resp)
class TestSortItems:
def test_sort_by_published_desc(self, fm_with_db):
fm = fm_with_db
older = datetime(2020, 1, 1, tzinfo=timezone.utc)
newer = datetime(2025, 6, 1, tzinfo=timezone.utc)
items = [
{"id": "a", "title": "old", "published": older, "raw": {}},
{"id": "b", "title": "new", "published": newer, "raw": {}},
]
out = fm._sort_items(items, {"field": "published", "order": "desc"})
assert [x["id"] for x in out] == ["b", "a"]
def test_sort_by_raw_numeric_timestamp_asc(self, fm_with_db):
fm = fm_with_db
items = [
{"id": "2", "title": "t2", "raw": {"t": 200.0}, "published": None},
{"id": "1", "title": "t1", "raw": {"t": 100.0}, "published": None},
]
out = fm._sort_items(items, {"field": "raw.t", "order": "asc"})
assert [x["id"] for x in out] == ["1", "2"]
def test_sort_empty_field_returns_unchanged(self, fm_with_db):
fm = fm_with_db
items = [{"id": "x", "title": "a"}]
out = fm._sort_items(items, {"field": "", "order": "desc"})
assert out == items
class TestFormatMessage:
def test_basic_placeholders(self, fm_with_db):
fm = fm_with_db
now = datetime.now(timezone.utc)
feed = {"output_format": "{emoji} {title}\n{link}\n{date}", "feed_name": "news"}
item = {
"title": "Hello",
"link": "https://ex.com/a",
"description": "",
"published": now,
"raw": {},
}
msg = fm.format_message(item, feed)
assert "Hello" in msg
assert "https://ex.com/a" in msg
assert "📢" in msg or "ℹ️" in msg # emoji from feed_name or default
def test_link_dict_href_coerced_like_feedparser(self, fm_with_db):
fm = fm_with_db
fm.shorten_feed_urls = False
feed = {"output_format": "{link}", "feed_name": "x"}
item = {
"title": "t",
"link": {"href": "https://ex.com/from-dict"},
"description": "",
"published": None,
"raw": {},
}
assert fm.format_message(item, feed) == "https://ex.com/from-dict"
def test_feed_name_null_from_db_does_not_crash(self, fm_with_db):
fm = fm_with_db
feed = {"output_format": "{emoji}{title}", "feed_name": None}
item = {
"title": "Hi",
"link": "",
"description": "",
"published": None,
"raw": {},
}
msg = fm.format_message(item, feed)
assert "Hi" in msg
def test_strips_br_and_html_from_body(self, fm_with_db):
fm = fm_with_db
feed = {"output_format": "{body}"}
item = {
"title": "t",
"description": 'Line1
Line2
Para
', "published": None, "raw": {}, } msg = fm.format_message(item, feed) assert "