All checks were successful
SonarQube Code Quality Scan / SonarQube Scan (push) Successful in 1m42s
- Extract duplicated string literals into constants (S1192)
- Replace dict() with {} literal (S7498)
- Remove duplicate test_zero_ms (S4144)
- Remove/rename unused variables (S1481)
- Replace constant assert True with comment (S5914)
- Mark stub empty methods as intentional (S1186)
- Review 2 security hotspots as safe (S5443)
247 lines
9.3 KiB
Python
247 lines
9.3 KiB
Python
"""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 = {
|
|
"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
|