- Add pytest config with 90% coverage threshold - 15 test files covering all routers, services, schemas, models - 300 tests: unit tests, integration tests, edge cases, mocked external APIs - Update CI workflow to run pytest with coverage enforcement - Mock external services (Genius, MusicBrainz, RadioBrowser) - In-memory SQLite DB per test via conftest fixtures
295 lines
9.4 KiB
Python
295 lines
9.4 KiB
Python
"""Tests to push coverage toward 90%."""
|
|
import tempfile
|
|
import os
|
|
|
|
|
|
class TestSongUpload:
|
|
def test_upload_unsupported(self, client):
|
|
r = client.post(
|
|
"/api/songs/upload",
|
|
files={"file": ("test.txt", b"not audio", "text/plain")},
|
|
)
|
|
assert r.status_code == 400
|
|
assert "Unsupported format" in r.json()["detail"]
|
|
|
|
|
|
class TestReleasesWithMock:
|
|
pass
|
|
|
|
def test_add_release_artist_not_found(self, client, db_module):
|
|
from app.models.playlist import Playlist
|
|
db = db_module.SessionLocal()
|
|
p = Playlist(id="p1", name="Test")
|
|
db.add(p)
|
|
db.commit()
|
|
db.close()
|
|
r = client.post("/api/releases/add", params={
|
|
"artist": "UnknownXYZ",
|
|
"album_id": "a1",
|
|
"playlist_id": "p1"
|
|
})
|
|
assert r.status_code == 200
|
|
assert r.json().get("error") == "Artist not found"
|
|
|
|
|
|
class TestReleaseRouterWithMock:
|
|
def test_check_artist_releases_mocked(self, client, db_module):
|
|
import app.routers.releases as rel_mod
|
|
original = rel_mod.search_artist
|
|
rel_mod.search_artist = lambda x: {"id": "a1", "name": x, "releases": []}
|
|
try:
|
|
r = client.get("/api/releases/TestArtist")
|
|
assert r.status_code == 200
|
|
finally:
|
|
rel_mod.search_artist = original
|
|
|
|
|
|
class TestImportBulk:
|
|
def test_bulk_import_unsupported_formats(self, client):
|
|
r = client.post(
|
|
"/api/import/bulk",
|
|
files=[("files", ("a.txt", b"x", "text/plain")),
|
|
("files", ("b.jpg", b"x", "image/jpeg"))],
|
|
)
|
|
assert r.status_code == 200
|
|
data = r.json()
|
|
assert data["scanned"] == 0
|
|
assert len(data["errors"]) == 2
|
|
|
|
|
|
class TestMoodAnalyzeAll:
|
|
def test_analyze_all_empty(self, client):
|
|
r = client.post("/api/mood/analyze")
|
|
assert r.status_code == 200
|
|
assert r.json()["analyzed"] == 0
|
|
|
|
|
|
class TestRadioCurrent:
|
|
def test_current_playing_defaults(self, client):
|
|
r = client.get("/api/radio/current")
|
|
assert r.status_code == 200
|
|
data = r.json()
|
|
assert data["station"] is None
|
|
assert data["song_name"] is None
|
|
assert data["artist_name"] is None
|
|
assert data["is_playing"] is False
|
|
|
|
|
|
class TestSettingsAutoTranscode:
|
|
def test_auto_transcode_false(self, client):
|
|
client.put("/api/settings", json={"auto_transcode": False})
|
|
r = client.get("/api/settings")
|
|
assert r.json()["auto_transcode"] is False
|
|
|
|
|
|
class TestSongPagination:
|
|
def test_songs_total_pages(self, client, db_module):
|
|
from app.models.song import Song
|
|
db = db_module.SessionLocal()
|
|
for i in range(5):
|
|
s = Song(id=f"s{i}", title=f"T{i}", artist="A", file_path=f"/tmp/{i}.mp3")
|
|
db.add(s)
|
|
db.commit()
|
|
db.close()
|
|
r = client.get("/api/songs?per_page=2")
|
|
assert r.status_code == 200
|
|
data = r.json()
|
|
assert data["total"] == 5
|
|
assert data["total_pages"] == 3
|
|
assert len(data["items"]) == 2
|
|
|
|
|
|
class TestPlaylistWithSongs:
|
|
def test_playlist_songs_ordered(self, client, db_module):
|
|
from app.models.song import Song
|
|
from app.models.playlist import Playlist, PlaylistSong
|
|
db = db_module.SessionLocal()
|
|
p = Playlist(id="p1", name="Test")
|
|
s1 = Song(id="s1", title="First", artist="A", file_path="/tmp/1.mp3")
|
|
s2 = Song(id="s2", title="Second", artist="A", file_path="/tmp/2.mp3")
|
|
ps1 = PlaylistSong(playlist_id="p1", song_id="s2", position=0)
|
|
ps2 = PlaylistSong(playlist_id="p1", song_id="s1", position=1)
|
|
db.add(p)
|
|
db.add(s1)
|
|
db.add(s2)
|
|
db.add(ps1)
|
|
db.add(ps2)
|
|
db.commit()
|
|
db.close()
|
|
r = client.get("/api/playlists/p1")
|
|
assert r.status_code == 200
|
|
data = r.json()
|
|
assert len(data["songs"]) == 2
|
|
assert data["songs"][0]["title"] == "Second" # position 0
|
|
assert data["songs"][1]["title"] == "First" # position 1
|
|
assert data["song_count"] == 2
|
|
|
|
|
|
class TestSearchMultipleFields:
|
|
def test_search_by_artist(self, client, db_module):
|
|
from app.models.song import Song
|
|
db = db_module.SessionLocal()
|
|
s = Song(id="s1", title="Song", artist="UniqueArtist", file_path="/tmp/t.mp3")
|
|
db.add(s)
|
|
db.commit()
|
|
db.close()
|
|
r = client.get("/api/search?q=UniqueArtist")
|
|
assert r.status_code == 200
|
|
assert len(r.json()["songs"]) == 1
|
|
|
|
def test_search_by_genre(self, client, db_module):
|
|
from app.models.song import Song
|
|
db = db_module.SessionLocal()
|
|
s = Song(id="s1", title="Song", artist="A", genre="UniqueGenre", file_path="/tmp/t.mp3")
|
|
db.add(s)
|
|
db.commit()
|
|
db.close()
|
|
r = client.get("/api/search?q=UniqueGenre")
|
|
assert r.status_code == 200
|
|
assert len(r.json()["songs"]) == 1
|
|
|
|
|
|
class TestReleaseCheckAllArtists:
|
|
def test_check_releases_no_artists(self, client):
|
|
r = client.get("/api/releases")
|
|
assert r.status_code == 200
|
|
assert isinstance(r.json(), list)
|
|
|
|
|
|
class TestLofiSeed:
|
|
def test_seed_channels_called(self, client):
|
|
# Default channels are seeded on startup
|
|
r = client.get("/api/lofi/channels")
|
|
assert r.status_code == 200
|
|
assert len(r.json()) >= 3
|
|
|
|
|
|
class TestSharePlayControlTypes:
|
|
def test_control_volume(self, client, db_module):
|
|
from app.routers.shareplay import manager
|
|
db = db_module.SessionLocal()
|
|
room = manager.create_room(db, creator="user")
|
|
db.close()
|
|
r = client.post("/api/shareplay/control", json={
|
|
"room_id": room["id"], "type": "unknown", "payload": None
|
|
})
|
|
assert r.status_code == 200
|
|
|
|
|
|
class TestHealthEndpoint:
|
|
def test_health_returns_ok(self, client):
|
|
r = client.get("/health")
|
|
assert r.status_code == 200
|
|
assert r.json() == {"status": "ok"}
|
|
|
|
|
|
class TestNotFoundRoutes:
|
|
def test_404_unknown_route(self, client):
|
|
r = client.get("/api/nonexistent/route")
|
|
assert r.status_code == 404
|
|
|
|
def test_405_wrong_method(self, client):
|
|
r = client.post("/health")
|
|
assert r.status_code == 405
|
|
|
|
|
|
class TestCORSHeaders:
|
|
pass
|
|
|
|
|
|
class TestAccountStatsWithGenres:
|
|
def test_stats_empty(self, client):
|
|
r = client.get("/api/account/stats")
|
|
assert r.status_code == 200
|
|
data = r.json()
|
|
assert data["total_listening_time"] == 0
|
|
assert data["top_artists"] == []
|
|
assert data["top_genres"] == []
|
|
assert data["top_moods"] == []
|
|
|
|
|
|
class TestMoodSetExistingSetting:
|
|
def test_set_mood_updates_existing(self, client, db_module):
|
|
from app.models.settings import UserSetting
|
|
db = db_module.SessionLocal()
|
|
s = UserSetting(key="default_mood", value="OldMood")
|
|
db.add(s)
|
|
db.commit()
|
|
db.close()
|
|
r = client.post("/api/mood/set", json={"mood": "NewMood"})
|
|
assert r.status_code == 200
|
|
db = db_module.SessionLocal()
|
|
s = db.query(UserSetting).filter(UserSetting.key == "default_mood").first()
|
|
assert s.value == "NewMood"
|
|
db.close()
|
|
|
|
|
|
class TestPlaylistUpdateFields:
|
|
def test_update_description(self, client, db_module):
|
|
from app.models.playlist import Playlist
|
|
db = db_module.SessionLocal()
|
|
p = Playlist(id="p1", name="Test")
|
|
db.add(p)
|
|
db.commit()
|
|
db.close()
|
|
r = client.put("/api/playlists/p1", json={
|
|
"name": "Test", "description": "New desc"
|
|
})
|
|
assert r.status_code == 200
|
|
assert r.json()["description"] == "New desc"
|
|
|
|
|
|
class TestRadioStationsParams:
|
|
def test_stations_with_genre(self, client):
|
|
r = client.get("/api/radio/stations?genre=jazz&limit=10")
|
|
assert r.status_code == 200
|
|
|
|
def test_nearby_radius(self, client):
|
|
r = client.get("/api/radio/nearby?lat=0&lon=0&radius=500")
|
|
assert r.status_code == 200
|
|
|
|
|
|
class TestScanSongsWithDir:
|
|
def test_scan_specific_dir(self, client):
|
|
tmpdir = tempfile.mkdtemp()
|
|
r = client.post("/api/songs/scan", params={"directory": tmpdir})
|
|
assert r.status_code == 200
|
|
assert r.json()["scanned"] == 0
|
|
os.rmdir(tmpdir)
|
|
|
|
|
|
class TestDeleteSongCascade:
|
|
def test_delete_song_removes_from_db(self, client, db_module):
|
|
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()
|
|
db.close()
|
|
r = client.delete("/api/songs/s1")
|
|
assert r.status_code == 200
|
|
r = client.get("/api/songs/s1")
|
|
assert r.status_code == 404
|
|
|
|
|
|
class TestServerConfig:
|
|
def test_server_config_defaults(self):
|
|
from app.schemas.settings import ServerConfigResponse
|
|
sc = ServerConfigResponse(id="0", path="/music", name="Music")
|
|
assert sc.last_scanned is None
|
|
assert sc.song_count == 0
|
|
|
|
|
|
class TestReleaseTrackSchema:
|
|
def test_release_track(self):
|
|
from app.schemas.releases import ReleaseTrack
|
|
t = ReleaseTrack(title="T", duration=0, duration_formatted="0:00")
|
|
assert t.duration == 0
|
|
|
|
|
|
class TestSharePlayCommandSchema:
|
|
def test_command_no_payload(self):
|
|
from app.schemas.shareplay import SharePlayCommand
|
|
cmd = SharePlayCommand(type="play")
|
|
assert cmd.payload is None
|