music-app/backend/tests/test_services.py
Jarian Cottingham 5c283eceef chore: remove test artifacts, fix hardcoded paths, ruff clean, license
- Remove committed .coverage and test-results/ (196K screenshots); gitignore them
- Fix hardcoded  /home/userpath + venv/bin/python in test_endpoints.py
  (relative backend dir + sys.executable)
- Fix concatenated 'import json' in settings.py; bare excepts -> Exception;
  SQLAlchemy-safe is_active.is_(True); __all__ on models/schemas barrels
- ruff clean (93 fixes), MIT LICENSE, PLAN.md -> docs/, README Tests section
- 300 tests pass, 93.7% coverage
2026-08-20 21:34:15 +00:00

304 lines
11 KiB
Python

"""Unit tests for mood engine and audio services."""
import pytest
import sys
import os
import tempfile
import shutil
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from app.services.mood_engine import analyze_lyrics, MOOD_NAMES, CONFIDENCE_THRESHOLD
class TestMoodEngine:
"""Test the mood classification engine."""
def test_analyze_sad_lyrics(self):
lyrics = "I cry alone in the dark, tears falling down, my heart is broken and I feel so lonely"
scores = analyze_lyrics(lyrics)
assert scores["Sad"] > 0
assert scores["Sad"] >= scores["Happy"]
def test_analyze_happy_lyrics(self):
lyrics = "I'm so happy and joyful, dancing in the sunshine, having fun and feeling free"
scores = analyze_lyrics(lyrics)
assert scores["Happy"] > 0
assert scores["Happy"] >= scores["Sad"]
def test_analyze_energetic_lyrics(self):
lyrics = "Fire burning, power rising, strong and fast, thunder and storm"
scores = analyze_lyrics(lyrics)
assert scores["Energetic"] > 0
assert scores["Energetic"] >= scores["Chill"]
def test_analyze_romantic_lyrics(self):
lyrics = "I love you baby, my heart beats for you, forever together, my darling"
scores = analyze_lyrics(lyrics)
assert scores["Romantic"] > 0
assert scores["Romantic"] >= scores["Angry"]
def test_analyze_angry_lyrics(self):
lyrics = "I feel rage and fury, hate and betrayal, screaming and destroying everything"
scores = analyze_lyrics(lyrics)
assert scores["Angry"] > 0
assert scores["Angry"] >= scores["Happy"]
def test_analyze_chill_lyrics(self):
lyrics = "Just relax and chill, smooth vibes and easy groove, mellow and laid back"
scores = analyze_lyrics(lyrics)
assert scores["Chill"] > 0
assert scores["Chill"] >= scores["Energetic"]
def test_analyze_focused_lyrics(self):
lyrics = "Clear mind, deep flow, steady focus and control, peace and quiet"
scores = analyze_lyrics(lyrics)
assert scores["Focused"] > 0
def test_analyze_nostalgic_lyrics(self):
lyrics = "I remember the past, yesterday was golden, childhood memories at home"
scores = analyze_lyrics(lyrics)
assert scores["Nostalgic"] > 0
assert scores["Nostalgic"] >= scores["Energetic"]
def test_analyze_melancholy_lyrics(self):
lyrics = "Sorrow and grief, fading shadows, silence and cold, heavy darkness"
scores = analyze_lyrics(lyrics)
assert scores["Melancholy"] > 0
assert scores["Melancholy"] >= scores["Happy"]
def test_analyze_dreamy_lyrics(self):
lyrics = "Dreaming of stars and sky, floating through space, magic and wonder, ethereal glow"
scores = analyze_lyrics(lyrics)
assert scores["Dreamy"] > 0
assert scores["Dreamy"] >= scores["Angry"]
def test_empty_lyrics(self):
scores = analyze_lyrics("")
for mood in MOOD_NAMES:
assert scores[mood] == 0.0
def test_none_lyrics(self):
scores = analyze_lyrics(None)
for mood in MOOD_NAMES:
assert scores[mood] == 0.0
def test_scores_are_normalized(self):
lyrics = "I cry alone in the dark, tears falling down"
scores = analyze_lyrics(lyrics)
max_score = max(scores.values())
assert max_score <= 1.0
def test_all_moods_present(self):
scores = analyze_lyrics("test lyrics here")
assert len(scores) == len(MOOD_NAMES)
for mood in MOOD_NAMES:
assert mood in scores
def test_mixed_mood_lyrics(self):
lyrics = "I'm happy but also sad, crying tears of joy, dancing alone"
scores = analyze_lyrics(lyrics)
assert scores["Happy"] > 0 or scores["Sad"] > 0
def test_case_insensitive(self):
scores_lower = analyze_lyrics("I cry alone in the dark")
scores_upper = analyze_lyrics("I CRY ALONE IN THE DARK")
assert scores_lower == scores_upper
def test_confidence_threshold_constant(self):
assert 0 < CONFIDENCE_THRESHOLD < 1
def test_mood_names_list(self):
expected = ["Sad", "Happy", "Energetic", "Focused", "Chill", "Romantic", "Angry", "Nostalgic", "Melancholy", "Dreamy"]
assert MOOD_NAMES == expected
def test_long_lyrics(self):
lyrics = " ".join(["word"] * 10000)
scores = analyze_lyrics(lyrics)
assert len(scores) == len(MOOD_NAMES)
def test_special_characters_in_lyrics(self):
lyrics = "I cry! alone... in the dark? tears!!"
scores = analyze_lyrics(lyrics)
assert "Sad" in scores
class TestAudioService:
"""Test audio utility functions."""
def test_supported_formats(self):
from app.services.audio import SUPPORTED_FORMATS
expected = {'.mp3', '.aac', '.flac', '.wav', '.ogg', '.m4a'}
assert SUPPORTED_FORMATS == expected
def test_extract_metadata_returns_dict(self):
from app.services.audio import extract_metadata
result = extract_metadata("/nonexistent/file.mp3")
assert isinstance(result, dict)
def test_extract_metadata_nonexistent_file(self):
from app.services.audio import extract_metadata
result = extract_metadata("/nonexistent/path/file.mp3")
assert result.get("title") is None
assert result.get("artist") is None
def test_transcode_nonexistent_file(self):
from app.services.audio import transcode_to_ogg
result = transcode_to_ogg("/nonexistent/file.mp3", "/tmp")
assert result is None
def test_get_stream_path_priority(self):
from app.services.audio import get_stream_path
from unittest.mock import MagicMock
song = MagicMock()
song.transcoded_path = "/tmp/existing.ogg"
song.file_path = "/tmp/original.mp3"
# If transcoded path exists, it should be preferred
os.makedirs("/tmp", exist_ok=True)
with open("/tmp/existing.ogg", "w") as f:
f.write("")
result = get_stream_path(song)
assert result == "/tmp/existing.ogg"
os.remove("/tmp/existing.ogg")
def test_get_stream_path_fallback(self):
from app.services.audio import get_stream_path
from unittest.mock import MagicMock
song = MagicMock()
song.transcoded_path = None
song.file_path = "/nonexistent/file.mp3"
result = get_stream_path(song)
assert result is None
def test_scan_empty_directory(self):
from app.services.audio import scan_directory
with tempfile.TemporaryDirectory() as tmpdir:
# Need a mock db
from unittest.mock import MagicMock
mock_db = MagicMock()
mock_db.query.return_value.count.return_value = 0
result = scan_directory(tmpdir, mock_db)
assert result.scanned == 0
assert result.added == 0
assert result.errors == []
def test_scan_skips_non_audio_files(self):
from app.services.audio import scan_directory
with tempfile.TemporaryDirectory() as tmpdir:
# Create non-audio files
with open(os.path.join(tmpdir, "readme.txt"), "w") as f:
f.write("test")
with open(os.path.join(tmpdir, "image.jpg"), "w") as f:
f.write("test")
from unittest.mock import MagicMock
mock_db = MagicMock()
mock_db.query.return_value.count.return_value = 0
result = scan_directory(tmpdir, mock_db)
assert result.scanned == 0
def test_delete_song_removes_files(self):
from app.services.audio import delete_song
tmpdir = tempfile.mkdtemp()
file1 = os.path.join(tmpdir, "test1.ogg")
file2 = os.path.join(tmpdir, "test2.mp3")
open(file1, "w").close()
open(file2, "w").close()
from unittest.mock import MagicMock
song = MagicMock()
song.file_path = file1
song.transcoded_path = file2
song.album_art_path = None
delete_song(song)
assert not os.path.exists(file1)
assert not os.path.exists(file2)
shutil.rmtree(tmpdir)
class TestRadioBrowser:
"""Test radio browser service."""
def test_haversine_same_point(self):
from app.services.radio_browser import _haversine
distance = _haversine(40.0, -74.0, 40.0, -74.0)
assert distance == 0.0
def test_haversine_positive_distance(self):
from app.services.radio_browser import _haversine
distance = _haversine(40.7128, -74.0060, 51.5074, -0.1278)
assert distance > 0
assert distance < 20000 # NYC to London < 20000km
def test_haversine_symmetric(self):
from app.services.radio_browser import _haversine
d1 = _haversine(40.0, -74.0, 51.0, 0.0)
d2 = _haversine(51.0, 0.0, 40.0, -74.0)
assert abs(d1 - d2) < 0.01
class TestLyricsService:
"""Test lyrics service."""
def test_no_api_key_returns_none(self):
from app.services.lyrics import fetch_lyrics
result = fetch_lyrics("test song", "test artist")
assert result is None
def test_fetch_lyrics_returns_string_or_none(self):
from app.services.lyrics import fetch_lyrics
result = fetch_lyrics("song", "artist")
assert result is None or isinstance(result, str)
class TestSharePlay:
"""Test SharePlay service."""
def test_manager_creation(self):
from app.services.shareplay import SharePlayManager
mgr = SharePlayManager()
assert mgr.rooms == {}
assert mgr.connections == {}
def test_create_room(self):
from app.services.shareplay import SharePlayManager
from unittest.mock import MagicMock
mgr = SharePlayManager()
mock_db = MagicMock()
result = mgr.create_room(mock_db, creator="user1")
assert "id" in result
assert result["creator_user"] == "user1"
assert result["active_connections"] == 1
assert result["id"] in mgr.rooms
def test_update_state(self):
from app.services.shareplay import SharePlayManager
mgr = SharePlayManager()
mgr.rooms["test"] = {"is_playing": False, "position": 0}
result = mgr.update_state("test", is_playing=True)
assert result["is_playing"] is True
def test_get_nonexistent_state(self):
from app.services.shareplay import SharePlayManager
mgr = SharePlayManager()
result = mgr.get_state("nonexistent")
assert result is None
if __name__ == "__main__":
pytest.main([__file__, "-v"])