import pytest from unittest.mock import MagicMock, AsyncMock, patch from datetime import datetime, timedelta from app.ingest import PlaybackTracker, finalize_track class TestPlaybackTracker: def test_initial_state(self): tracker = PlaybackTracker() assert tracker.current_track_id is None assert tracker.track_start_time is None assert tracker.accumulated_listen_ms == 0 assert tracker.last_progress_ms == 0 assert tracker.is_paused is False class TestFinalizeTrack: def test_finalize_creates_play_history_when_not_exists(self): mock_db = MagicMock() mock_db.query.return_value.filter.return_value.first.return_value = None tracker = PlaybackTracker() tracker.current_track_id = "track123" tracker.track_start_time = datetime(2024, 1, 1, 10, 0, 0) tracker.accumulated_listen_ms = 60000 finalize_track(mock_db, tracker) mock_db.add.assert_called_once() mock_db.commit.assert_called_once() assert tracker.current_track_id is None assert tracker.accumulated_listen_ms == 0 def test_finalize_marks_skip_when_under_30s(self): mock_db = MagicMock() mock_db.query.return_value.filter.return_value.first.return_value = None tracker = PlaybackTracker() tracker.current_track_id = "track123" tracker.track_start_time = datetime(2024, 1, 1, 10, 0, 0) tracker.accumulated_listen_ms = 15000 finalize_track(mock_db, tracker) call_args = mock_db.add.call_args[0][0] assert call_args.skipped is True def test_finalize_updates_existing_play(self): mock_existing = MagicMock() mock_existing.listened_ms = None mock_db = MagicMock() mock_db.query.return_value.filter.return_value.first.return_value = ( mock_existing ) tracker = PlaybackTracker() tracker.current_track_id = "track123" tracker.track_start_time = datetime(2024, 1, 1, 10, 0, 0) tracker.accumulated_listen_ms = 120000 finalize_track(mock_db, tracker) assert mock_existing.listened_ms == 120000 assert mock_existing.skipped is False mock_db.commit.assert_called_once() class TestReccoBeatsClient: @pytest.mark.asyncio async def test_extracts_spotify_id_from_href(self): from app.services.reccobeats_client import ReccoBeatsClient with patch("httpx.AsyncClient") as mock_client: mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = { "content": [ { "id": "uuid-here", "href": "https://open.spotify.com/track/abc123xyz", "energy": 0.8, "valence": 0.6, } ] } mock_client.return_value.__aenter__.return_value.get = AsyncMock( return_value=mock_response ) client = ReccoBeatsClient() result = await client.get_audio_features(["abc123xyz"]) assert len(result) == 1 assert result[0]["spotify_id"] == "abc123xyz" assert result[0]["energy"] == 0.8 @pytest.mark.asyncio async def test_returns_empty_on_error(self): from app.services.reccobeats_client import ReccoBeatsClient with patch("httpx.AsyncClient") as mock_client: mock_client.return_value.__aenter__.return_value.get = AsyncMock( side_effect=Exception("Network error") ) client = ReccoBeatsClient() result = await client.get_audio_features(["test123"]) assert result == []