"""Tests for the music database module (music_db.py).""" import json import os import tempfile import pytest # We'll create a fresh MusicDB for each test using a temporary file. @pytest.fixture def music_db(): """Yield a MusicDB backed by a temporary SQLite file.""" from music_db import MusicDB tmp = tempfile.NamedTemporaryFile(suffix=".db", delete=False) tmp.close() db = MusicDB(tmp.name) yield db db.close() os.unlink(tmp.name) # ── Helper ──────────────────────────────────────────────────────────────────── _SONG = dict( video_id="test_video_001", title="Test Song", artists=[{"name": "Test Artist", "id": "artist_001"}], album={"name": "Test Album", "id": "album_001"}, duration=240, thumbnail="https://example.com/thumb.jpg", ) def _record_play(db, **overrides): kwargs = dict(_SONG) kwargs.update(overrides) db.record_play(**kwargs) # ── Tests ───────────────────────────────────────────────────────────────────── class TestRecordPlay: def test_records_play_and_history(self, music_db): """record_play creates a song entry and a history row.""" _record_play(music_db) stats = music_db.get_stats() assert stats["unique_songs"] == 1 assert stats["total_plays"] == 1 history = music_db.get_history(limit=10) assert len(history) == 1 assert history[0]["video_id"] == _SONG["video_id"] def test_increments_play_count(self, music_db): """Playing the same song multiple times increments play_count.""" _record_play(music_db) _record_play(music_db) _record_play(music_db) stats = music_db.get_stats() assert stats["unique_songs"] == 1 assert stats["total_plays"] == 3 def test_updates_last_played(self, music_db): """Re-playing a song updates last_played timestamp.""" _record_play(music_db) import time time.sleep(0.01) _record_play(music_db) history = music_db.get_history(limit=10) # The latest play should be the one returned assert history[0]["video_id"] == _SONG["video_id"] def test_without_album(self, music_db): """record_play works without album data.""" _record_play(music_db, album=None) stats = music_db.get_stats() assert stats["unique_songs"] == 1 def test_without_thumbnail(self, music_db): """record_play works without thumbnail.""" _record_play(music_db, thumbnail=None) stats = music_db.get_stats() assert stats["unique_songs"] == 1 class TestFavourites: def test_toggle_favourite(self, music_db): """toggle_favourite flips the favourite flag.""" _record_play(music_db) # Start unfavoured assert music_db.is_favourite(_SONG["video_id"]) is False # Toggle on assert music_db.toggle_favourite(_SONG["video_id"]) is True assert music_db.is_favourite(_SONG["video_id"]) is True # Toggle off assert music_db.toggle_favourite(_SONG["video_id"]) is False assert music_db.is_favourite(_SONG["video_id"]) is False def test_toggle_favourite_nonexistent_song(self, music_db): """toggle_favourite on a non-existent song returns False.""" assert music_db.toggle_favourite("nonexistent") is False def test_set_favourite(self, music_db): """set_favourite explicitly sets the favourite state.""" _record_play(music_db) vid = _SONG["video_id"] assert music_db.set_favourite(vid, True) is True assert music_db.is_favourite(vid) is True assert music_db.set_favourite(vid, False) is True assert music_db.is_favourite(vid) is False def test_set_favourite_nonexistent(self, music_db): """set_favourite on a non-existent song returns False.""" assert music_db.set_favourite("nonexistent", True) is False def test_get_favourites_returns_favourited_songs(self, music_db): """get_favourites returns only favourited songs.""" # Record two songs, favourite one _record_play(music_db, video_id="song_a", title="A") _record_play(music_db, video_id="song_b", title="B") music_db.toggle_favourite("song_a") favs = music_db.get_favourites() assert len(favs) == 1 assert favs[0]["video_id"] == "song_a" class TestHistory: def test_history_ordered_by_recent(self, music_db): """get_history returns songs ordered by most recent play.""" _record_play(music_db, video_id="song_a", title="A") import time time.sleep(0.01) _record_play(music_db, video_id="song_b", title="B") history = music_db.get_history(limit=10) # Most recent (song_b) should be first assert history[0]["video_id"] == "song_b" assert history[1]["video_id"] == "song_a" def test_history_deduplicates_by_video_id(self, music_db): """get_history shows each song at most once (most recent play).""" _record_play(music_db, video_id="song_a", title="A") _record_play(music_db, video_id="song_b", title="B") _record_play(music_db, video_id="song_a", title="A") history = music_db.get_history(limit=10) # song_a appears once, but song_b is in there too assert len(history) == 2 # song_a is most recent since it was played last assert history[0]["video_id"] == "song_a" def test_history_respects_limit(self, music_db): """get_history respects the limit parameter.""" for i in range(5): _record_play(music_db, video_id=f"song_{i}", title=f"S{i}") history = music_db.get_history(limit=3) assert len(history) == 3 def test_empty_history(self, music_db): """get_history on empty DB returns empty list.""" assert music_db.get_history() == [] class TestStats: def test_get_stats_empty(self, music_db): """get_stats on empty DB returns zero counts.""" stats = music_db.get_stats() assert stats["unique_songs"] == 0 assert stats["total_plays"] is None or stats["total_plays"] == 0 assert stats["favourites"] == 0 assert stats["top_played"] == [] assert stats["recent"] == [] assert stats["plays_by_day"] == [] def test_get_stats_with_data(self, music_db): """get_stats returns accurate aggregate data.""" _record_play(music_db, video_id="song_a", title="A", duration=120) _record_play(music_db, video_id="song_b", title="B", duration=180) _record_play(music_db, video_id="song_a", title="A", duration=120) music_db.toggle_favourite("song_b") stats = music_db.get_stats() assert stats["unique_songs"] == 2 assert stats["total_plays"] == 3 assert stats["favourites"] == 1 assert stats["top_played"][0]["video_id"] == "song_a" assert stats["top_played"][0]["play_count"] == 2 assert len(stats["recent"]) == 3 assert len(stats["plays_by_day"]) >= 1 # today at least def test_top_played_limit_returns_up_to_ten(self, music_db): """top_played in stats returns at most 10 entries.""" for i in range(15): _record_play(music_db, video_id=f"song_{i}", title=f"S{i}") for _ in range(i + 1): _record_play(music_db, video_id=f"song_{i}", title=f"S{i}") stats = music_db.get_stats() assert len(stats["top_played"]) == 10 class TestEdgeCases: def test_unicode_song_title(self, music_db): """record_play handles Unicode characters in titles and artists.""" _record_play(music_db, title="🎵 Джаз", artists=[{"name": "José", "id": "j"}]) history = music_db.get_history(limit=10) assert history[0]["title"] == "🎵 Джаз" def test_empty_artists_list(self, music_db): """record_play works with empty artists list.""" _record_play(music_db, artists=[]) stats = music_db.get_stats() assert stats["unique_songs"] == 1 def test_very_long_title(self, music_db): """record_play handles very long titles.""" long_title = "A" * 1000 _record_play(music_db, title=long_title) history = music_db.get_history(limit=10) assert history[0]["title"] == long_title def test_concurrent_access(self, music_db): """Multiple rapid calls don't cause errors.""" import threading errors = [] def worker(vid): try: _record_play(music_db, video_id=vid, title=f"S{vid}") music_db.toggle_favourite(vid) music_db.is_favourite(vid) music_db.get_stats() except Exception as e: errors.append(e) threads = [threading.Thread(target=worker, args=(f"vid_{i}",)) for i in range(20)] for t in threads: t.start() for t in threads: t.join(timeout=5) assert not errors, f"Concurrent errors: {errors}" stats = music_db.get_stats() assert stats["unique_songs"] == 20