music-app/backend/tests/test_deep_coverage.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

460 lines
17 KiB
Python

"""Tests for deep coverage of services and middleware."""
from unittest.mock import MagicMock, patch
import tempfile
import os
class TestAPIKeyMiddleware:
def test_health_no_auth(self, client):
r = client.get("/health")
assert r.status_code == 200
def test_static_no_auth(self, client):
r = client.get("/static/test.txt")
assert r.status_code in (200, 404) # 404 ok if file missing
class TestMoodEngineDeep:
def test_seed_mood_categories(self, db_module):
from app.services.mood_engine import seed_mood_categories
from app.models.mood import MoodCategory
db = db_module.SessionLocal()
seed_mood_categories(db)
cats = db.query(MoodCategory).all()
assert len(cats) == 10
names = [c.name for c in cats]
assert "Happy" in names
assert "Sad" in names
db.close()
def test_seed_idempotent(self, db_module):
from app.services.mood_engine import seed_mood_categories
from app.models.mood import MoodCategory
db = db_module.SessionLocal()
seed_mood_categories(db)
seed_mood_categories(db)
cats = db.query(MoodCategory).all()
assert len(cats) == 10
db.close()
def test_analyze_song_mood_no_lyrics(self, db_module):
from app.services.mood_engine import analyze_song_mood
from app.models.song import Song
db = db_module.SessionLocal()
s = Song(id="s1", title="T", artist="A", file_path="/tmp/t.mp3")
db.add(s)
db.commit()
result = analyze_song_mood("s1", db)
assert result["song_id"] == "s1"
assert result["top_mood"] is None
assert result["confidence"] == 0
db.close()
def test_get_mood_playlist_with_songs(self, db_module):
from app.services.mood_engine import get_mood_playlist
from app.models.song import Song
from app.models.mood import MoodSong
db = db_module.SessionLocal()
s = Song(id="s1", title="T", artist="A", file_path="/tmp/t.mp3")
ms = MoodSong(mood_id="happy", song_id="s1", confidence_score=0.9)
db.add(s)
db.add(ms)
db.commit()
songs = get_mood_playlist("happy", db, limit=50)
assert len(songs) == 1
db.close()
class TestLyricsServiceMocked:
def test_fetch_lyrics_no_key(self):
from app.services.lyrics import fetch_lyrics, GENIUS_API_KEY
assert GENIUS_API_KEY == ""
assert fetch_lyrics("song", "artist") is None
def test_fetch_lyrics_request_error(self):
import app.services.lyrics as lyrics_mod
original_key = lyrics_mod.GENIUS_API_KEY
lyrics_mod.GENIUS_API_KEY = "fake_key"
import requests
with patch.object(requests, "get", side_effect=requests.RequestException("fail")):
result = lyrics_mod.fetch_lyrics("song", "artist")
assert result is None
lyrics_mod.GENIUS_API_KEY = original_key
def test_fetch_lyrics_no_hits(self):
import app.services.lyrics as lyrics_mod
original_key = lyrics_mod.GENIUS_API_KEY
lyrics_mod.GENIUS_API_KEY = "fake_key"
import requests
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.json.return_value = {"response": {"hits": []}}
with patch.object(requests, "get", return_value=mock_resp):
result = lyrics_mod.fetch_lyrics("song", "artist")
assert result is None
lyrics_mod.GENIUS_API_KEY = original_key
def test_fetch_lyrics_non_200(self):
import app.services.lyrics as lyrics_mod
original_key = lyrics_mod.GENIUS_API_KEY
lyrics_mod.GENIUS_API_KEY = "fake_key"
import requests
mock_resp = MagicMock()
mock_resp.status_code = 401
with patch.object(requests, "get", return_value=mock_resp):
result = lyrics_mod.fetch_lyrics("song", "artist")
assert result is None
lyrics_mod.GENIUS_API_KEY = original_key
def test_fetch_lyrics_no_song_id(self):
import app.services.lyrics as lyrics_mod
original_key = lyrics_mod.GENIUS_API_KEY
lyrics_mod.GENIUS_API_KEY = "fake_key"
import requests
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.json.return_value = {"response": {"hits": [{"result": {}}]}}
with patch.object(requests, "get", return_value=mock_resp):
result = lyrics_mod.fetch_lyrics("song", "artist")
assert result is None
lyrics_mod.GENIUS_API_KEY = original_key
def test_fetch_lyrics_empty_lyrics(self):
import app.services.lyrics as lyrics_mod
original_key = lyrics_mod.GENIUS_API_KEY
lyrics_mod.GENIUS_API_KEY = "fake_key"
import requests
search_resp = MagicMock()
search_resp.status_code = 200
search_resp.json.return_value = {"response": {"hits": [{"result": {"id": 123}}]}}
song_resp = MagicMock()
song_resp.status_code = 200
song_resp.json.return_value = {"response": {"lyrics": ""}}
with patch.object(requests, "get", side_effect=[search_resp, song_resp]):
result = lyrics_mod.fetch_lyrics("song", "artist")
assert result is None
lyrics_mod.GENIUS_API_KEY = original_key
def test_fetch_lyrics_not_written_yet(self):
import app.services.lyrics as lyrics_mod
original_key = lyrics_mod.GENIUS_API_KEY
lyrics_mod.GENIUS_API_KEY = "fake_key"
import requests
search_resp = MagicMock()
search_resp.status_code = 200
search_resp.json.return_value = {"response": {"hits": [{"result": {"id": 123}}]}}
song_resp = MagicMock()
song_resp.status_code = 200
song_resp.json.return_value = {"response": {"lyrics": "[Lyrics are not written yet]"}}
with patch.object(requests, "get", side_effect=[search_resp, song_resp]):
result = lyrics_mod.fetch_lyrics("song", "artist")
assert result is None
lyrics_mod.GENIUS_API_KEY = original_key
def test_fetch_lyrics_success(self):
import app.services.lyrics as lyrics_mod
original_key = lyrics_mod.GENIUS_API_KEY
lyrics_mod.GENIUS_API_KEY = "fake_key"
import requests
search_resp = MagicMock()
search_resp.status_code = 200
search_resp.json.return_value = {"response": {"hits": [{"result": {"id": 123}}]}}
song_resp = MagicMock()
song_resp.status_code = 200
song_resp.json.return_value = {"response": {"lyrics": "test lyrics here"}}
with patch.object(requests, "get", side_effect=[search_resp, song_resp]):
result = lyrics_mod.fetch_lyrics("song", "artist")
assert result == "test lyrics here"
lyrics_mod.GENIUS_API_KEY = original_key
class TestMusicbrainzMocked:
def test_search_artist_404(self):
import app.services.musicbrainz as mb
import requests
mock_resp = MagicMock()
mock_resp.status_code = 404
with patch.object(requests, "get", return_value=mock_resp):
result = mb.search_artist("unknown")
assert result is None
def test_search_artist_no_results(self):
import app.services.musicbrainz as mb
import requests
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.json.return_value = {"artists": []}
with patch.object(requests, "get", return_value=mock_resp):
result = mb.search_artist("unknown")
assert result is None
def test_search_artist_success(self):
import app.services.musicbrainz as mb
import requests
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.json.return_value = {"artists": [{"id": "a1", "name": "Test Artist"}]}
with patch.object(requests, "get", return_value=mock_resp):
result = mb.search_artist("Test")
assert result["id"] == "a1"
assert result["name"] == "Test Artist"
def test_get_artist_releases_404(self):
import app.services.musicbrainz as mb
import requests
mock_resp = MagicMock()
mock_resp.status_code = 404
with patch.object(requests, "get", return_value=mock_resp):
result = mb.get_artist_releases("a1")
assert result == []
def test_get_artist_releases_success(self):
import app.services.musicbrainz as mb
import requests
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.json.return_value = {
"releases": [
{
"id": "r1",
"title": "Album 1",
"date": "2025-01-01",
"media": [{"tracks": [{"title": "Song 1", "length": 245000}]}]
}
]
}
with patch.object(requests, "get", return_value=mock_resp):
result = mb.get_artist_releases("a1")
assert len(result) == 1
assert result[0]["title"] == "Album 1"
assert len(result[0]["tracks"]) == 1
def test_get_artist_releases_error(self):
import app.services.musicbrainz as mb
import requests
with patch.object(requests, "get", side_effect=requests.RequestException("fail")):
result = mb.get_artist_releases("a1")
assert result == []
class TestAudioServiceDeep:
def test_scan_existing_skipped(self, db_module):
from app.services.audio import scan_directory
from app.models.song import Song
tmpdir = tempfile.mkdtemp()
# Create a fake audio file
fp = os.path.join(tmpdir, "test.mp3")
with open(fp, "wb") as f:
f.write(b"fake mp3")
db = db_module.SessionLocal()
# Pre-add to DB
s = Song(id="s1", title="T", artist="A", file_path=fp)
db.add(s)
db.commit()
result = scan_directory(tmpdir, db)
assert result.scanned == 1
assert result.added == 0
db.close()
os.unlink(fp)
os.rmdir(tmpdir)
def test_transcode_timeout(self):
from app.services.audio import transcode_to_ogg
import subprocess
with patch("app.services.audio.subprocess.run", side_effect=subprocess.TimeoutExpired("cmd", 1)):
result = transcode_to_ogg("/tmp/test.mp3", "/tmp")
assert result is None
def test_transcode_file_not_found(self):
from app.services.audio import transcode_to_ogg
with patch("app.services.audio.subprocess.run", side_effect=FileNotFoundError()):
result = transcode_to_ogg("/tmp/test.mp3", "/tmp")
assert result is None
class TestSharePlayManagerDeep:
def test_add_to_cue_success(self, db_module):
from app.services.shareplay import SharePlayManager
from app.models.song import Song
db = db_module.SessionLocal()
s = Song(id="s1", title="T", artist="A", file_path="/tmp/t.mp3")
db.add(s)
db.commit()
m = SharePlayManager()
result = m.add_to_cue(db, "room1", "s1", "alice")
assert result is True
db.close()
def test_next_cue_success(self, db_module):
from app.services.shareplay import SharePlayManager
from app.models.song import Song
from app.models.shareplay import SharePlayCue as CueModel
db = db_module.SessionLocal()
s = Song(id="s1", title="T", artist="A", file_path="/tmp/t.mp3")
cue = CueModel(id="c1", room_id="room1", song_id="s1", position=0, added_by="alice")
db.add(s)
db.add(cue)
db.commit()
m = SharePlayManager()
result = m.next_cue(db, "room1")
assert result == "s1"
db.close()
def test_get_cue_with_songs(self, db_module):
from app.services.shareplay import SharePlayManager
from app.models.song import Song
from app.models.shareplay import SharePlayCue as CueModel
db = db_module.SessionLocal()
s = Song(id="s1", title="Test Song", artist="Artist A", file_path="/tmp/t.mp3")
cue = CueModel(id="c1", room_id="room1", song_id="s1", position=0, added_by="alice")
db.add(s)
db.add(cue)
db.commit()
m = SharePlayManager()
result = m.get_cue(db, "room1")
assert len(result) == 1
assert result[0]["song"]["title"] == "Test Song"
assert result[0]["added_by"] == "alice"
db.close()
class TestMoodKeywords:
def test_all_moods_have_15_keywords(self):
from app.services.mood_engine import MOOD_KEYWORDS
for mood, keywords in MOOD_KEYWORDS.items():
assert len(keywords) == 15, f"{mood} has {len(keywords)} keywords"
def test_all_weights_positive(self):
from app.services.mood_engine import MOOD_KEYWORDS
for mood, keywords in MOOD_KEYWORDS.items():
for word, weight in keywords:
assert weight > 0, f"{mood}/{word} has non-positive weight"
def test_keywords_are_words(self):
from app.services.mood_engine import MOOD_KEYWORDS
for mood, keywords in MOOD_KEYWORDS.items():
for word, weight in keywords:
assert " " not in word, f"{mood}/{word} contains space"
assert word == word.lower(), f"{mood}/{word} not lowercase"
class TestDefaultChannels:
def test_default_channels_count(self):
from app.routers.lofi import DEFAULT_CHANNELS
assert len(DEFAULT_CHANNELS) == 3
def test_default_channels_have_fields(self):
from app.routers.lofi import DEFAULT_CHANNELS
for ch in DEFAULT_CHANNELS:
assert "id" in ch
assert "name" in ch
assert "stream_url" in ch
assert ch.get("is_active") is True
class TestPlaceholderEvents:
def test_placeholder_events_count(self):
from app.routers.events import PLACEHOLDER_EVENTS
assert len(PLACEHOLDER_EVENTS) >= 3
def test_placeholder_events_have_fields(self):
from app.routers.events import PLACEHOLDER_EVENTS
for e in PLACEHOLDER_EVENTS:
assert "id" in e
assert "name" in e
assert "date" in e
class TestRadioBrowserFormatEdge:
def test_format_station_none_geo(self):
from app.services.radio_browser import _format_station
s = {
"stationuuid": "u1",
"name": "Test",
"codec": "mp3",
"url_resolved": "http://s.com",
"geo_lat": None,
"geo_long": None,
"tag": "",
"countryname": "",
"language": "",
"bitrate": None,
"votes": None,
}
result = _format_station(s)
assert result["location_lat"] is None
assert result["bitrate"] is None
assert result["votes"] == 0
def test_format_station_empty_tags(self):
from app.services.radio_browser import _format_station
s = {
"stationuuid": "u1", "name": "T", "codec": "mp3",
"url_resolved": "http://s.com", "geo_lat": None, "geo_long": None,
"tag": None, "countryname": "", "language": "",
"bitrate": None, "votes": None,
}
result = _format_station(s)
assert result["tags"] == []
class TestHaversineEdgeCases:
def test_poles(self):
from app.services.radio_browser import _haversine
d = _haversine(90, 0, -90, 0)
assert 19000 < d < 21000 # ~20000km pole to pole
def test_equator(self):
from app.services.radio_browser import _haversine
d = _haversine(0, 0, 0, 180)
assert 19000 < d < 21000 # ~20000km equator half-circle
class TestDurationFormat:
def test_zero(self):
from app.services.musicbrainz import _format_duration
assert _format_duration(0) == "0:00"
def test_under_minute(self):
from app.services.musicbrainz import _format_duration
assert _format_duration(45) == "0:45"
def test_exact_minute(self):
from app.services.musicbrainz import _format_duration
assert _format_duration(60) == "1:00"
def test_over_hour(self):
from app.services.musicbrainz import _format_duration
assert _format_duration(3725) == "62:05"
class TestScanResultSchema:
def test_scan_result_defaults(self):
from app.schemas.song import ScanResult
sr = ScanResult(scanned=0, added=0, skipped=0, errors=[])
assert sr.errors == []
def test_scan_result_with_errors(self):
from app.schemas.song import ScanResult
sr = ScanResult(scanned=5, added=3, skipped=1, errors=["err1", "err2"])
assert len(sr.errors) == 2
class TestUserSettingsDefaults:
def test_all_defaults(self):
from app.schemas.settings import UserSettingsResponse
us = UserSettingsResponse()
assert us.audio_quality == "high"
assert us.theme == "dark"
assert us.scan_directories == []
assert us.user_name == "User"
assert us.user_avatar is None
assert us.auto_transcode is True
assert us.default_mood is None