- 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
299 lines
11 KiB
Python
299 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
"""Comprehensive backend endpoint tests for music-app."""
|
|
import httpx
|
|
import sys
|
|
import os
|
|
import time
|
|
import subprocess
|
|
|
|
BASE_URL = "http://localhost:8000"
|
|
passed = 0
|
|
failed = 0
|
|
errors = []
|
|
|
|
def test(name, condition, detail=""):
|
|
global passed, failed
|
|
if condition:
|
|
passed += 1
|
|
print(f" ✅ {name}")
|
|
else:
|
|
failed += 1
|
|
errors.append(name)
|
|
print(f" ❌ {name} {detail}")
|
|
|
|
def start_server():
|
|
"""Start the backend server."""
|
|
# backend/ directory (parent of this tests/ dir)
|
|
os.chdir(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
# Remove old DB
|
|
if os.path.exists("app.db"):
|
|
os.remove("app.db")
|
|
|
|
proc = subprocess.Popen(
|
|
[sys.executable, "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"],
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
)
|
|
time.sleep(3)
|
|
return proc
|
|
|
|
def stop_server(proc):
|
|
proc.terminate()
|
|
proc.wait(timeout=5)
|
|
|
|
def main():
|
|
global passed, failed
|
|
|
|
print("🎵 Music App Backend Tests")
|
|
print("=" * 50)
|
|
|
|
proc = start_server()
|
|
|
|
try:
|
|
client = httpx.Client(base_url=BASE_URL, timeout=10)
|
|
|
|
# 1. Health check
|
|
print("\n📋 Health & Basic")
|
|
r = client.get("/health")
|
|
test("Health endpoint returns 200", r.status_code == 200, f"got {r.status_code}")
|
|
test("Health returns status ok", r.json().get("status") == "ok")
|
|
|
|
# 2. Songs endpoints
|
|
print("\n🎵 Songs")
|
|
r = client.get("/api/songs")
|
|
test("GET /api/songs returns 200", r.status_code == 200)
|
|
test("Songs list has pagination fields", "items" in r.json() and "total" in r.json())
|
|
test("Empty songs list", r.json()["total"] == 0)
|
|
|
|
r = client.get("/api/songs?page=1&per_page=20")
|
|
test("Songs pagination params work", r.status_code == 200)
|
|
|
|
# Test 404 for non-existent song
|
|
r = client.get("/api/songs/nonexistent-id")
|
|
test("GET non-existent song returns 404", r.status_code == 404)
|
|
|
|
# 3. Playlists endpoints
|
|
print("\n📀 Playlists")
|
|
r = client.get("/api/playlists")
|
|
test("GET /api/playlists returns 200", r.status_code == 200)
|
|
test("Empty playlists list", r.json() == [])
|
|
|
|
# Create playlist
|
|
r = client.post("/api/playlists", json={"name": "Test Playlist", "description": "A test playlist"})
|
|
test("POST /api/playlists creates playlist", r.status_code == 200)
|
|
playlist_id = r.json().get("id", "")
|
|
test("Created playlist has ID", len(playlist_id) > 0)
|
|
|
|
# Get playlist
|
|
r = client.get(f"/api/playlists/{playlist_id}")
|
|
test("GET playlist by ID returns 200", r.status_code == 200)
|
|
test("Playlist has songs field", "songs" in r.json())
|
|
|
|
# Update playlist
|
|
r = client.put(f"/api/playlists/{playlist_id}", json={"name": "Updated Name"})
|
|
test("PUT /api/playlists updates name", r.status_code == 200)
|
|
test("Name was updated", r.json()["name"] == "Updated Name")
|
|
|
|
# Create second playlist
|
|
r = client.post("/api/playlists", json={"name": "Second Playlist"})
|
|
test("Create second playlist", r.status_code == 200)
|
|
playlist_id_2 = r.json().get("id", "")
|
|
|
|
# List playlists
|
|
r = client.get("/api/playlists")
|
|
test("List shows 2 playlists", len(r.json()) == 2)
|
|
|
|
# Share playlist
|
|
r = client.post(f"/api/playlists/{playlist_id}/share")
|
|
test("Share playlist returns token", r.status_code == 200 and "token" in r.json())
|
|
token = r.json().get("token", "")
|
|
|
|
# Access shared playlist
|
|
if token:
|
|
r = client.get(f"/api/playlists/shared/{token}")
|
|
test("Access shared playlist", r.status_code == 200)
|
|
|
|
# Delete playlist
|
|
r = client.delete(f"/api/playlists/{playlist_id_2}")
|
|
test("DELETE playlist returns 200", r.status_code == 200)
|
|
|
|
r = client.get("/api/playlists")
|
|
test("One playlist remains after delete", len(r.json()) == 1)
|
|
|
|
# 4. Mood endpoints
|
|
print("\n🎭 Mood Radio")
|
|
r = client.get("/api/mood/categories")
|
|
test("GET mood categories returns 200", r.status_code == 200)
|
|
categories = r.json()
|
|
test("Has 10 mood categories", len(categories) == 10)
|
|
mood_names = [c["name"] for c in categories]
|
|
test("Has Sad mood", "Sad" in mood_names)
|
|
test("Has Happy mood", "Happy" in mood_names)
|
|
test("Has Energetic mood", "Energetic" in mood_names)
|
|
test("Categories have color_hex", all("color_hex" in c for c in categories))
|
|
|
|
# Get mood playlist
|
|
r = client.get("/api/mood/Sad/playlist")
|
|
test("GET mood playlist returns 200", r.status_code == 200)
|
|
test("Mood playlist has songs field", "songs" in r.json())
|
|
test("Mood playlist has total_songs field", "total_songs" in r.json())
|
|
|
|
# Set mood
|
|
r = client.post("/api/mood/set", json={"mood": "Happy"})
|
|
test("Set mood returns 200", r.status_code == 200)
|
|
test("Mood was set", r.json().get("mood") == "Happy")
|
|
|
|
# Analyze mood (library-wide)
|
|
r = client.post("/api/mood/analyze")
|
|
test("Analyze mood returns 200", r.status_code == 200)
|
|
|
|
# 5. LoFi endpoints
|
|
print("\n🎧 LoFi Channels")
|
|
r = client.get("/api/lofi/channels")
|
|
test("GET lofi channels returns 200", r.status_code == 200)
|
|
channels = r.json()
|
|
test("Has 3 lofi channels", len(channels) == 3)
|
|
test("Channels have stream_url", all("stream_url" in c for c in channels))
|
|
|
|
# Add channel
|
|
r = client.post("/api/lofi/add", json={
|
|
"name": "Test Channel",
|
|
"stream_url": "https://example.com/stream",
|
|
"description": "Test"
|
|
})
|
|
test("Add lofi channel returns 200", r.status_code == 200)
|
|
|
|
r = client.get("/api/lofi/channels")
|
|
test("Now has 4 channels", len(r.json()) == 4)
|
|
|
|
# 6. Radio endpoints
|
|
print("\n📻 Internet Radio")
|
|
r = client.get("/api/radio/stations?limit=5")
|
|
test("GET radio stations returns 200", r.status_code == 200)
|
|
stations = r.json()
|
|
test("Stations is a list", isinstance(stations, list))
|
|
|
|
r = client.get("/api/radio/nearby?lat=40.7&lon=-74.0&radius=100")
|
|
test("GET nearby stations returns 200", r.status_code == 200)
|
|
|
|
r = client.get("/api/radio/current")
|
|
test("GET current radio returns 200", r.status_code == 200)
|
|
|
|
# 7. Search endpoints
|
|
print("\n🔍 Search")
|
|
r = client.get("/api/search?q=test")
|
|
test("GET search returns 200", r.status_code == 200)
|
|
data = r.json()
|
|
test("Search has songs field", "songs" in data)
|
|
test("Search has playlists field", "playlists" in data)
|
|
test("Search has query field", data["query"] == "test")
|
|
test("Search has total_results field", "total_results" in data)
|
|
|
|
# 8. Settings endpoints
|
|
print("\n⚙️ Settings")
|
|
r = client.get("/api/settings")
|
|
test("GET settings returns 200", r.status_code == 200)
|
|
test("Settings has audio_quality", "audio_quality" in r.json())
|
|
test("Settings has theme", "theme" in r.json())
|
|
|
|
r = client.put("/api/settings", json={"audio_quality": "medium", "theme": "light"})
|
|
test("PUT settings returns 200", r.status_code == 200)
|
|
|
|
r = client.get("/api/settings")
|
|
test("Settings were updated", r.json()["audio_quality"] == "medium")
|
|
|
|
# Server config
|
|
r = client.get("/api/settings/servers")
|
|
test("GET servers returns 200", r.status_code == 200)
|
|
|
|
# 9. SharePlay endpoints
|
|
print("\n👥 SharePlay")
|
|
r = client.post("/api/shareplay/create")
|
|
test("Create shareplay room returns 200", r.status_code == 200)
|
|
room_id = r.json().get("id", "")
|
|
test("Room has ID", len(room_id) > 0)
|
|
|
|
r = client.post("/api/shareplay/join", json={"room_id": room_id})
|
|
test("Join shareplay room returns 200", r.status_code == 200)
|
|
|
|
r = client.get(f"/api/shareplay/cue?room_id={room_id}")
|
|
test("Get cue returns 200", r.status_code == 200)
|
|
|
|
r = client.post("/api/shareplay/control", json={"room_id": room_id, "type": "play"})
|
|
test("Send play control returns 200", r.status_code == 200)
|
|
|
|
r = client.post("/api/shareplay/leave", json={"room_id": room_id})
|
|
test("Leave room returns 200", r.status_code == 200)
|
|
|
|
# 10. Events endpoints
|
|
print("\n🎪 Events")
|
|
r = client.get("/api/events")
|
|
test("GET events returns 200", r.status_code == 200)
|
|
events = r.json()
|
|
test("Has 3 placeholder events", len(events) == 3)
|
|
test("Events have name field", all("name" in e for e in events))
|
|
|
|
# 11. Releases endpoints
|
|
print("\n🆕 New Releases")
|
|
r = client.get("/api/releases")
|
|
test("GET releases returns 200", r.status_code == 200)
|
|
|
|
# 12. Account endpoints
|
|
print("\n👤 Account")
|
|
r = client.get("/api/account/stats")
|
|
test("GET account stats returns 200", r.status_code == 200)
|
|
stats = r.json()
|
|
test("Stats has total_songs", "total_songs" in stats)
|
|
test("Stats has total_playlists", "total_playlists" in stats)
|
|
|
|
r = client.get("/api/account/history")
|
|
test("GET account history returns 200", r.status_code == 200)
|
|
|
|
# 13. Song scan
|
|
print("\n📂 Song Scan")
|
|
r = client.post("/api/songs/scan?directory=./music")
|
|
test("Scan empty directory returns 200", r.status_code == 200)
|
|
scan = r.json()
|
|
test("Scan has scanned field", "scanned" in scan)
|
|
test("Scan has added field", "added" in scan)
|
|
|
|
# 14. Import endpoints
|
|
print("\n📥 Import")
|
|
# Bulk import test (would need actual files)
|
|
test("Import endpoint exists (manual file test needed)", True)
|
|
|
|
# 15. CORS headers
|
|
print("\n🌐 CORS")
|
|
r = client.get("/api/songs", headers={"Origin": "http://localhost:5173"})
|
|
test("CORS headers present", "access-control-allow-origin" in r.headers)
|
|
|
|
# 16. Error handling
|
|
print("\n🛡️ Error Handling")
|
|
r = client.get("/api/nonexistent")
|
|
test("Non-existent route returns 404", r.status_code == 404)
|
|
|
|
r = client.delete("/api/songs/nonexistent")
|
|
test("Delete non-existent song returns 404", r.status_code == 404)
|
|
|
|
r = client.get("/api/playlists/nonexistent")
|
|
test("Get non-existent playlist returns 404", r.status_code == 404)
|
|
|
|
# Summary
|
|
print("\n" + "=" * 50)
|
|
total = passed + failed
|
|
print(f"\n📊 Results: {passed}/{total} passed, {failed} failed")
|
|
|
|
if errors:
|
|
print("\n❌ Failed tests:")
|
|
for e in errors:
|
|
print(f" - {e}")
|
|
|
|
print(f"\n✅ Coverage: {passed/total*100:.0f}% of tested endpoints working")
|
|
|
|
finally:
|
|
stop_server(proc)
|
|
|
|
return 0 if failed == 0 else 1
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main()) |