"""Tests for RSS feed polling.""" from datetime import datetime, timezone from pathlib import Path import httpx import pytest from unittest.mock import AsyncMock from services.meet_kevin_watcher.rss_poller import DiscoveredVideo, parse_feed, fetch_feed @pytest.fixture def fixture_xml() -> bytes: """Load real YouTube RSS feed fixture.""" fixture_path = Path(__file__).parent.parent.parent / "fixtures" / "meet_kevin_rss.xml" return fixture_path.read_bytes() class TestParseFeed: """Test parse_feed with various inputs.""" def test_parse_feed_real_fixture(self, fixture_xml: bytes): """parse_feed returns videos from real YouTube RSS feed.""" videos = parse_feed(fixture_xml) # Should return multiple videos (fixture has 15 entries) assert len(videos) >= 1 assert isinstance(videos, list) # Each video should have correct structure video = videos[0] assert isinstance(video, DiscoveredVideo) assert len(video.youtube_video_id) == 11 assert isinstance(video.title, str) and len(video.title) > 0 assert isinstance(video.description, str) assert isinstance(video.published_at, datetime) assert video.published_at.tzinfo is not None assert video.thumbnail_url.startswith("https://") def test_parse_feed_empty_input(self): """parse_feed returns empty list on empty input.""" result = parse_feed(b"") assert result == [] def test_parse_feed_invalid_xml(self): """parse_feed returns empty list on malformed XML.""" result = parse_feed(b"") assert result == [] def test_parse_feed_missing_fields(self): """parse_feed skips entries with missing required fields.""" # Valid feed header but entry missing video_id xml = b""" Video without ID 2026-05-21T15:22:13+00:00 Test """ result = parse_feed(xml) assert result == [] def test_discovered_video_is_frozen(self): """DiscoveredVideo is frozen (immutable).""" video = DiscoveredVideo( youtube_video_id="abc123abc12", title="Test", description="Test desc", published_at=datetime(2026, 5, 21, tzinfo=timezone.utc), thumbnail_url="https://example.com/img.jpg", ) # Should be hashable (frozen=True) assert hash(video) is not None # Should not be mutable with pytest.raises(AttributeError): video.title = "Changed" # type: ignore class TestFetchFeed: """Test fetch_feed with HTTP client.""" @pytest.mark.asyncio async def test_fetch_feed_success(self): """fetch_feed returns bytes on successful HTTP GET.""" mock_response = AsyncMock() mock_response.content = b"data" # raise_for_status is synchronous, so don't mock it as async mock_response.raise_for_status = lambda: None mock_client = AsyncMock(spec=httpx.AsyncClient) mock_client.get.return_value = mock_response result = await fetch_feed("UCUvvj5lwue7PspotMDjk5UA", mock_client) assert result == b"data" mock_client.get.assert_called_once() # Check timeout is set to 15.0 call_kwargs = mock_client.get.call_args[1] assert call_kwargs["timeout"] == 15.0 @pytest.mark.asyncio async def test_fetch_feed_http_error(self): """fetch_feed returns empty bytes on HTTP error.""" mock_client = AsyncMock(spec=httpx.AsyncClient) mock_client.get.side_effect = httpx.HTTPError("Connection failed") result = await fetch_feed("UCUvvj5lwue7PspotMDjk5UA", mock_client) assert result == b"" @pytest.mark.asyncio async def test_fetch_feed_uses_correct_url(self): """fetch_feed constructs correct YouTube RSS feed URL.""" mock_response = AsyncMock() mock_response.content = b"" # raise_for_status is synchronous, so don't mock it as async mock_response.raise_for_status = lambda: None mock_client = AsyncMock(spec=httpx.AsyncClient) mock_client.get.return_value = mock_response await fetch_feed("UCUvvj5lwue7PspotMDjk5UA", mock_client) # Verify URL was constructed correctly call_args = mock_client.get.call_args[0] assert "https://www.youtube.com/feeds/videos.xml" in call_args[0] assert "channel_id=UCUvvj5lwue7PspotMDjk5UA" in call_args[0] @pytest.mark.asyncio async def test_fetch_feed_timeout_error(self): """fetch_feed returns empty bytes on timeout.""" mock_client = AsyncMock(spec=httpx.AsyncClient) mock_client.get.side_effect = httpx.TimeoutException("Timeout") result = await fetch_feed("UCUvvj5lwue7PspotMDjk5UA", mock_client) assert result == b""