import asyncio from datetime import datetime from unittest.mock import AsyncMock, MagicMock import pytest from ntr_fetcher.poller import Poller from ntr_fetcher.models import Track def _make_track(id: int, liked_at: str) -> Track: return Track( id=id, title=f"Track {id}", artist="Artist", permalink_url=f"https://soundcloud.com/a/t-{id}", artwork_url=None, duration_ms=180000, license="cc-by", liked_at=datetime.fromisoformat(liked_at), raw_json="{}", ) @pytest.mark.asyncio async def test_poll_once_fetches_and_stores(): mock_sc = AsyncMock() mock_sc.resolve_user.return_value = 206979918 mock_sc.fetch_likes.return_value = [ _make_track(1, "2026-03-14T01:00:00+00:00"), _make_track(2, "2026-03-14T02:00:00+00:00"), ] mock_db = MagicMock() mock_show = MagicMock() mock_show.id = 1 mock_db.get_or_create_show.return_value = mock_show poller = Poller( db=mock_db, soundcloud=mock_sc, soundcloud_user="nicktherat", show_day=2, show_hour=22, poll_interval=3600, ) await poller.poll_once() assert mock_sc.resolve_user.called assert mock_sc.fetch_likes.called assert mock_db.upsert_track.call_count == 2 assert mock_db.set_show_tracks.called call_args = mock_db.set_show_tracks.call_args assert call_args[0][0] == 1 assert call_args[0][1] == [1, 2] @pytest.mark.asyncio async def test_poll_once_removes_unliked_tracks(): mock_sc = AsyncMock() mock_sc.resolve_user.return_value = 206979918 mock_sc.fetch_likes.return_value = [ _make_track(1, "2026-03-14T01:00:00+00:00"), _make_track(2, "2026-03-14T02:00:00+00:00"), ] mock_db = MagicMock() mock_show = MagicMock() mock_show.id = 1 mock_db.get_or_create_show.return_value = mock_show poller = Poller( db=mock_db, soundcloud=mock_sc, soundcloud_user="nicktherat", show_day=2, show_hour=22, poll_interval=3600, ) await poller.poll_once() mock_sc.fetch_likes.return_value = [ _make_track(1, "2026-03-14T01:00:00+00:00"), ] mock_db.reset_mock() mock_db.get_or_create_show.return_value = mock_show await poller.poll_once() call_args = mock_db.set_show_tracks.call_args assert call_args[0][1] == [1] @pytest.mark.asyncio async def test_poll_once_full_refresh(): mock_sc = AsyncMock() mock_sc.resolve_user.return_value = 206979918 mock_sc.fetch_likes.return_value = [ _make_track(1, "2026-03-14T01:00:00+00:00"), ] mock_db = MagicMock() mock_show = MagicMock() mock_show.id = 1 mock_db.get_or_create_show.return_value = mock_show poller = Poller( db=mock_db, soundcloud=mock_sc, soundcloud_user="nicktherat", show_day=2, show_hour=22, poll_interval=3600, ) await poller.poll_once(full=True) assert mock_sc.fetch_likes.called @pytest.mark.asyncio async def test_supervisor_restarts_poller_on_failure(): call_count = 0 async def failing_poll(**kwargs): nonlocal call_count call_count += 1 if call_count <= 2: raise RuntimeError("Simulated failure") mock_sc = AsyncMock() mock_db = MagicMock() poller = Poller( db=mock_db, soundcloud=mock_sc, soundcloud_user="nicktherat", show_day=2, show_hour=22, poll_interval=0.01, ) poller.poll_once = failing_poll task = asyncio.create_task(poller.run_supervised(restart_delay=0.01)) await asyncio.sleep(0.2) task.cancel() try: await task except asyncio.CancelledError: pass assert call_count >= 3