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

208 lines
7.1 KiB
Python

"""Extended service tests - Audio, Lyrics, Musicbrainz, RadioBrowser."""
from unittest.mock import patch, MagicMock
import tempfile
import os
class TestExtractMetadata:
def test_nonexistent_file(self):
from app.services.audio import extract_metadata
result = extract_metadata("/nonexistent/file.mp3")
assert isinstance(result, dict)
assert result.get("title") is None
def test_returns_dict(self):
from app.services.audio import extract_metadata
result = extract_metadata("/no/file.wav")
assert isinstance(result, dict)
def test_supported_formats(self):
from app.services.audio import SUPPORTED_FORMATS
assert SUPPORTED_FORMATS == {'.mp3', '.aac', '.flac', '.wav', '.ogg', '.m4a'}
class TestTranscode:
def test_nonexistent_input(self):
from app.services.audio import transcode_to_ogg
result = transcode_to_ogg("/nonexistent.mp3", "/tmp")
assert result is None
class TestScanDirectory:
def test_empty_dir(self):
from app.services.audio import scan_directory
with tempfile.TemporaryDirectory() as tmpdir:
mock_db = MagicMock()
mock_db.query.return_value.first.return_value = None
result = scan_directory(tmpdir, mock_db)
assert result.scanned == 0
assert result.added == 0
assert result.errors == []
def test_skips_non_audio(self):
from app.services.audio import scan_directory
with tempfile.TemporaryDirectory() as tmpdir:
with open(os.path.join(tmpdir, "readme.txt"), "w") as f:
f.write("test")
mock_db = MagicMock()
mock_db.query.return_value.first.return_value = None
result = scan_directory(tmpdir, mock_db)
assert result.scanned == 0
class TestDeleteSong:
def test_delete_files(self):
from app.services.audio import delete_song
tmpdir = tempfile.mkdtemp()
f1 = os.path.join(tmpdir, "t1.ogg")
f2 = os.path.join(tmpdir, "t2.mp3")
open(f1, "w").close()
open(f2, "w").close()
song = MagicMock()
song.file_path = f1
song.transcoded_path = f2
song.album_art_path = None
delete_song(song)
assert not os.path.exists(f1)
assert not os.path.exists(f2)
import shutil
shutil.rmtree(tmpdir)
def test_delete_no_paths(self):
from app.services.audio import delete_song
song = MagicMock()
song.file_path = None
song.transcoded_path = None
song.album_art_path = None
delete_song(song) # Should not raise
class TestGetStreamPath:
def test_transcoded_priority(self):
from app.services.audio import get_stream_path
tmpdir = tempfile.mkdtemp()
f1 = os.path.join(tmpdir, "t.ogg")
f2 = os.path.join(tmpdir, "t.mp3")
open(f1, "w").close()
open(f2, "w").close()
song = MagicMock()
song.transcoded_path = f1
song.file_path = f2
assert get_stream_path(song) == f1
import shutil
shutil.rmtree(tmpdir)
def test_fallback_original(self):
from app.services.audio import get_stream_path
tmpdir = tempfile.mkdtemp()
f = os.path.join(tmpdir, "t.mp3")
open(f, "w").close()
song = MagicMock()
song.transcoded_path = None
song.file_path = f
assert get_stream_path(song) == f
import shutil
shutil.rmtree(tmpdir)
def test_no_files(self):
from app.services.audio import get_stream_path
song = MagicMock()
song.transcoded_path = None
song.file_path = None
assert get_stream_path(song) is None
class TestLyricsService:
def test_no_api_key(self):
from app.services.lyrics import fetch_lyrics
assert fetch_lyrics("song", "artist") is None
def test_returns_none_or_str(self):
from app.services.lyrics import fetch_lyrics
result = fetch_lyrics("s", "a")
assert result is None or isinstance(result, str)
class TestMusicbrainz:
def test_search_returns_none_no_network(self):
from app.services.musicbrainz import search_artist
result = search_artist("nonexistent_xyz_artist_123")
assert result is None or isinstance(result, dict)
def test_releases_returns_none_no_network(self):
from app.services.musicbrainz import get_artist_releases
result = get_artist_releases("nonexistent-id")
assert isinstance(result, list)
def test_format_duration(self):
from app.services.musicbrainz import _format_duration
assert _format_duration(0) == "0:00"
assert _format_duration(60) == "1:00"
assert _format_duration(245) == "4:05"
assert _format_duration(3661) == "61:01"
def test_format_duration_negative(self):
from app.services.musicbrainz import _format_duration
assert _format_duration(-1) == "-1:59"
class TestRadioBrowser:
def test_fetch_stations_empty_on_error(self):
from app.services.radio_browser import fetch_stations
import requests as req_mod
with patch.object(req_mod, "get", side_effect=req_mod.RequestException("fail")):
result = fetch_stations(limit=5)
assert result == []
def test_fetch_nearby_empty_on_error(self):
from app.services.radio_browser import fetch_nearby_stations
import requests as req_mod
with patch.object(req_mod, "get", side_effect=req_mod.RequestException("fail")):
result = fetch_nearby_stations(40.7, -74.0, 100)
assert result == []
def test_format_station(self):
from app.services.radio_browser import _format_station
s = {
"stationuuid": "u1",
"name": "Test Radio",
"codec": "mp3",
"url_resolved": "http://stream.test",
"geo_lat": "40.7",
"geo_long": "-74.0",
"tag": "jazz,lofi",
"countryname": "USA",
"language": "en",
"bitrate": "128",
"votes": "10",
}
result = _format_station(s)
assert result["id"] == "u1"
assert result["name"] == "Test Radio"
assert result["stream_url"] == "http://stream.test"
assert result["location_lat"] == 40.7
assert result["tags"] == ["jazz", "lofi"]
assert result["bitrate"] == 128
assert result["votes"] == 10
def test_format_station_missing_fields(self):
from app.services.radio_browser import _format_station
s = {}
result = _format_station(s)
assert result["name"] == "Unknown"
def test_haversine_same(self):
from app.services.radio_browser import _haversine
assert _haversine(0, 0, 0, 0) == 0.0
def test_haversine_symmetric(self):
from app.services.radio_browser import _haversine
a = _haversine(40.7, -74.0, 51.5, -0.1)
b = _haversine(51.5, -0.1, 40.7, -74.0)
assert abs(a - b) < 0.01
def test_haversine_nyc_london(self):
from app.services.radio_browser import _haversine
d = _haversine(40.7128, -74.0060, 51.5074, -0.1278)
assert 5500 < d < 6000