EmpathVoice / tests /test_api.py
Ashu1007's picture
feat: complete v0.2.0 rewrite with hybrid prosody pipeline, React UI, and Docker deployment
4723136
Raw History Blame Contribute Delete
4.06 kB
"""API tests via TestClient with fakes injected through dependency overrides.
No torch, no network: the model manager and synthesizer are faked, and a real
temp-dir AudioStore exercises the full synthesize -> store -> retrieve path.
"""
from __future__ import annotations
import pytest
from app.api.deps import get_manager, get_store, get_synthesizer
from app.audio.store import AudioStore
from app.emotion import pipeline
from app.emotion.weights import build_emotion_matrix
from app.main import app
from app.ml.manager import ModelStatus
from tests.conftest import LABELS, FakeEmotionModel, FakeSynthesizer, FakeVADModel
class FakeManager:
def __init__(self, ready: bool = True) -> None:
self.status = ModelStatus.READY if ready else ModelStatus.FAILED
self.device = "cpu"
self.detail = None
self._w = build_emotion_matrix(LABELS)
@property
def ready(self) -> bool:
return self.status is ModelStatus.READY
async def analyze(self, text: str, expressiveness: float = 1.0):
return pipeline.analyze(
text, FakeEmotionModel(), FakeVADModel(), self._w, expressiveness
)
@pytest.fixture
def client(tmp_path):
from fastapi.testclient import TestClient
store = AudioStore(tmp_path, ttl_seconds=3600)
synth = FakeSynthesizer()
app.dependency_overrides[get_manager] = lambda: FakeManager(ready=True)
app.dependency_overrides[get_store] = lambda: store
app.dependency_overrides[get_synthesizer] = lambda: synth
with TestClient(app) as c:
c._synth = synth # expose for assertions
yield c
app.dependency_overrides.clear()
def test_health_always_ok(client):
r = client.get("/health")
assert r.status_code == 200 and r.json()["status"] == "ok"
def test_voices(client):
r = client.get("/api/voices")
assert r.status_code == 200
body = r.json()
assert body["default"]
assert any(v["id"] == body["default"] for v in body["voices"])
def test_analyze_happy_path(client):
r = client.post("/api/analyze", json={"text": "I am so happy right now"})
assert r.status_code == 200
body = r.json()
assert body["top_emotions"][0]["label"] == "joy"
assert set(body["prosody"]) == {"pitch_hz", "rate_pct", "energy_pct", "pause_level"}
assert "model estimates" in body["explanation"]
def test_analyze_rejects_empty(client):
r = client.post("/api/analyze", json={"text": " "})
assert r.status_code == 422
def test_analyze_rejects_too_long(client):
r = client.post("/api/analyze", json={"text": "a" * 5000})
assert r.status_code == 422
def test_synthesize_and_retrieve_audio(client):
r = client.post("/api/synthesize", json={"text": "hello there friend"})
assert r.status_code == 200
body = r.json()
assert body["audio_url"].startswith("/api/audio/")
assert body["expires_in_seconds"] == 3600
assert body["analysis"]["top_emotions"]
# The generated audio is retrievable at the returned url.
audio = client.get(body["audio_url"])
assert audio.status_code == 200
assert audio.headers["content-type"] == "audio/mpeg"
assert audio.content == b"ID3\x04fake-mp3-bytes"
def test_synthesize_rejects_unknown_voice(client):
r = client.post("/api/synthesize", json={"text": "hi", "voice": "xx-ZZ-NopeNeural"})
assert r.status_code == 400
assert r.json()["error"]["code"] == "invalid_request"
def test_audio_404_for_unknown_id(client):
r = client.get("/api/audio/deadbeefdeadbeefdeadbeefdeadbeef")
assert r.status_code == 404
assert r.json()["error"]["code"] == "audio_not_found"
def test_not_ready_returns_503():
from fastapi.testclient import TestClient
app.dependency_overrides[get_manager] = lambda: FakeManager(ready=False)
try:
with TestClient(app) as c:
r = c.post("/api/analyze", json={"text": "hello"})
assert r.status_code == 503
assert r.json()["error"]["code"] == "model_not_ready"
finally:
app.dependency_overrides.clear()