"""Tests for web application routes — login, settings, file management.""" import json import os import tempfile from pathlib import Path import pytest from fastapi import FastAPI from fastapi.testclient import TestClient from src.network.auth import APIKeyAuth, API_KEY_HEADER from src.network.session import SessionManager from src.network.web_routes import create_web_routes, get_static_files_app class TestLoginRoutes: """Tests for session auth login/logout endpoints.""" @pytest.fixture def auth(self): return APIKeyAuth(keys=["test-key-123"]) @pytest.fixture def session_mgr(self): return SessionManager(ttl=3600, max_sessions=10) @pytest.fixture def app(self, auth, session_mgr): app = FastAPI() routers = create_web_routes( auth=auth, session_manager=session_mgr, settings_getter=lambda: {}, settings_setter=lambda _: True, ) for router in routers: app.include_router(router) return app @pytest.fixture def client(self, app): return TestClient(app) def test_login_with_header(self, client): resp = client.post( "/api/v1/auth/login", headers={API_KEY_HEADER: "test-key-123"}, ) assert resp.status_code == 200 data = resp.json() assert "token" in data assert "expires_in" in data assert len(data["token"]) == 64 def test_login_with_json_body(self, client): resp = client.post( "/api/v1/auth/login", json={"api_key": "test-key-123"}, ) assert resp.status_code == 200 data = resp.json() assert "token" in data def test_login_invalid_key(self, client): resp = client.post( "/api/v1/auth/login", json={"api_key": "wrong-key"}, ) assert resp.status_code == 403 def test_login_missing_key(self, client): resp = client.post("/api/v1/auth/login") assert resp.status_code == 403 def test_login_no_body_or_header(self, client): resp = client.post( "/api/v1/auth/login", headers={}, ) assert resp.status_code == 403 def test_login_creates_session(self, client, session_mgr): assert session_mgr.active_sessions == 0 resp = client.post( "/api/v1/auth/login", headers={API_KEY_HEADER: "test-key-123"}, ) assert resp.status_code == 200 assert session_mgr.active_sessions == 1 def test_logout_valid_token(self, client, session_mgr): # Create a session first token = session_mgr.create_session("browser") assert session_mgr.active_sessions == 1 resp = client.post( "/api/v1/auth/logout", json={"token": token}, ) assert resp.status_code == 200 assert resp.json()["ok"] is True assert session_mgr.active_sessions == 0 def test_logout_invalid_token(self, client, session_mgr): resp = client.post( "/api/v1/auth/logout", json={"token": "not-a-real-token"}, ) assert resp.status_code == 200 assert resp.json()["ok"] is False def test_logout_missing_token(self, client): resp = client.post("/api/v1/auth/logout") assert resp.status_code == 400 def test_login_without_content_type(self, client): """Test login without JSON content-type header.""" resp = client.post( "/api/v1/auth/login", content="not json", headers={API_KEY_HEADER: "test-key-123"}, ) assert resp.status_code == 200 # Falls back to header auth class TestSettingsRoutes: """Tests for settings REST endpoints.""" @pytest.fixture def settings_state(self): return {"sample_rate": 48000, "buffer_size": 256, "volume_limit": -3.0} @pytest.fixture def auth(self): return APIKeyAuth(keys=["test-key"]) @pytest.fixture def session_mgr(self): return SessionManager() @pytest.fixture def app(self, auth, session_mgr, settings_state): app = FastAPI() def getter(): return dict(settings_state) def setter(updates): valid = set(settings_state.keys()) for k in updates: if k not in valid: return False settings_state.update(updates) return True routers = create_web_routes( auth=auth, session_manager=session_mgr, settings_getter=getter, settings_setter=setter, ) for router in routers: app.include_router(router) return app @pytest.fixture def client(self, app): return TestClient(app) def test_get_settings(self, client, settings_state): resp = client.get("/api/v1/settings") assert resp.status_code == 200 data = resp.json() assert data == settings_state def test_update_settings_valid(self, client, settings_state): resp = client.put( "/api/v1/settings", json={"buffer_size": 512}, ) assert resp.status_code == 200 assert resp.json()["ok"] is True assert settings_state["buffer_size"] == 512 def test_update_settings_invalid_key(self, client, settings_state): resp = client.put( "/api/v1/settings", json={"not_a_setting": 123}, ) assert resp.status_code == 400 def test_update_settings_invalid_body(self, client): resp = client.put( "/api/v1/settings", content="not json", ) assert resp.status_code == 400 def test_update_settings_not_a_dict(self, client): resp = client.put( "/api/v1/settings", json=[1, 2, 3], ) assert resp.status_code == 400 class TestFileRoutes: """Tests for file management REST endpoints.""" @pytest.fixture def audio_dir(self): with tempfile.TemporaryDirectory() as tmp: audio_path = Path(tmp) # Create some test audio files (audio_path / "track1.wav").write_text("fake wav data") (audio_path / "track2.mp3").write_text("fake mp3 data") (audio_path / "readme.txt").write_text("not audio") (audio_path / "track3.flac").write_text("fake flac data") yield audio_path @pytest.fixture def auth(self): return APIKeyAuth(keys=["test-key"]) @pytest.fixture def session_mgr(self): return SessionManager() @pytest.fixture def app(self, auth, session_mgr, audio_dir): app = FastAPI() routers = create_web_routes( auth=auth, session_manager=session_mgr, settings_getter=lambda: {}, settings_setter=lambda _: True, audio_dir=audio_dir, ) for router in routers: app.include_router(router) return app @pytest.fixture def client(self, app): return TestClient(app) def test_list_files(self, client): resp = client.get("/api/v1/files") assert resp.status_code == 200 data = resp.json() assert data["exists"] is True assert data["count"] == 3 # wav, mp3, flac (not .txt) names = {f["name"] for f in data["files"]} assert "track1.wav" in names assert "track2.mp3" in names assert "track3.flac" in names assert "readme.txt" not in names def test_download_audio_file(self, client): resp = client.get("/api/v1/files/track1.wav") assert resp.status_code == 200 assert resp.content == b"fake wav data" def test_download_nonexistent_file(self, client): resp = client.get("/api/v1/files/nope.wav") assert resp.status_code == 404 def test_download_non_audio_file(self, client): resp = client.get("/api/v1/files/readme.txt") assert resp.status_code == 403 def test_path_traversal_prevented(self, client): resp = client.get("/api/v1/files/../../../etc/passwd") assert resp.status_code in (403, 404) def test_nonexistent_directory(self): """Test listing files when directory doesn't exist.""" app = FastAPI() auth = APIKeyAuth(keys=["test-key"]) session_mgr = SessionManager() routers = create_web_routes( auth=auth, session_manager=session_mgr, settings_getter=lambda: {}, settings_setter=lambda _: True, audio_dir=Path("/tmp/nonexistent-audio-dir-xyz"), ) for router in routers: app.include_router(router) client = TestClient(app) resp = client.get("/api/v1/files") assert resp.status_code == 200 data = resp.json() assert data["exists"] is False assert data["files"] == [] class TestStaticFiles: """Tests for static file serving.""" def test_get_static_files_app(self): with tempfile.TemporaryDirectory() as tmp: web_dir = Path(tmp) (web_dir / "index.html").write_text("

Hello

") app = get_static_files_app(web_dir) assert app is not None def test_placeholder_created(self): """Test that placeholder UI is created when dir doesn't exist.""" with tempfile.TemporaryDirectory() as tmp: web_dir = Path(tmp) / "nonexistent-web" app = get_static_files_app(web_dir) assert web_dir.exists() index = web_dir / "index.html" assert index.exists() content = index.read_text() assert "RPi Audio Mixer" in content