astra-local-voice/tests/test_server.py
Jeuner 991f0d1eb5 feat: add voice picker UI and optional Tailnet exposure
Stimmen werden jetzt lazy pro Name geladen und gecacht statt einer
fest verdrahteten Default-Stimme; das UI bekommt ein Dropdown mit
allen 26 deutschen Pocket-TTS-Stimmen (/api/voices), Auswahl wird im
Browser gemerkt und ist während eines laufenden Gesprächs gesperrt.

Zusätzlich ein optionaler ASTRA_TAILNET_HOST, damit die App über
`tailscale serve` auch von einem anderen Gerät im selben Tailnet
erreichbar ist, ohne die Loopback-only-Härtung für alle anderen
Hosts aufzuweichen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVgSHNHdRx3UNTBodFmhRA
2026-09-07 15:15:13 +02:00

66 lines
2.9 KiB
Python

from fastapi.testclient import TestClient
from astra.server import create_app
def test_status_and_static_ui_work_before_models_are_ready():
with TestClient(create_app(load_models=False), base_url="http://localhost:7860") as client:
status = client.get("/api/status")
assert status.status_code == 200
assert status.json()["thinking"] is False
assert status.json()["ready"] is False
page = client.get("/")
assert "Einfach aussprechen" in page.text
assert "frame-ancestors 'none'" in page.headers["content-security-policy"]
assert client.get("/static/app.js").status_code == 200
def test_other_websites_cannot_open_a_microphone_session():
with TestClient(create_app(load_models=False), base_url="http://localhost:7860") as client:
for origin in ["https://evil.example", "null", "http://localhost.evil.example:7860"]:
response = client.post("/api/offer", headers={"Origin": origin}, json={})
assert response.status_code == 403
assert client.post("/api/offer", json={}).status_code == 403
def test_unready_returns_actionable_status_and_invalid_sdp_is_rejected():
with TestClient(create_app(load_models=False), base_url="http://localhost:7860") as client:
headers = {"Origin": "http://localhost:7860"}
assert client.post("/api/offer", headers=headers, json={}).status_code == 422
response = client.post(
"/api/offer", headers=headers, json={"sdp": "a" * 20, "type": "offer"}
)
assert response.status_code == 503
assert response.json()["detail"]
def test_voices_endpoint_lists_selectable_voices():
with TestClient(create_app(load_models=False), base_url="http://localhost:7860") as client:
response = client.get("/api/voices")
assert response.status_code == 200
body = response.json()
assert body["default"] == "alba"
names = {voice["name"] for voice in body["voices"]}
assert "alba" in names
assert all({"name", "display_name", "gender"} <= voice.keys() for voice in body["voices"])
def test_offer_rejects_unknown_voice():
with TestClient(create_app(load_models=False), base_url="http://localhost:7860") as client:
response = client.post(
"/api/offer",
headers={"Origin": "http://localhost:7860"},
json={"sdp": "a" * 20, "type": "offer", "voice": "not-a-real-voice"},
)
assert response.status_code == 422
def test_disconnect_is_idempotent_and_host_is_checked():
with TestClient(create_app(load_models=False), base_url="http://localhost:7860") as client:
response = client.post(
"/api/disconnect",
headers={"Origin": "http://localhost:7860"},
json={"pc_id": "already-gone"},
)
assert response.json() == {"ok": True}
assert client.get("/api/status", headers={"Host": "evil.example"}).status_code == 403