- 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
208 lines
7.1 KiB
Python
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
|