Files
talkscore-asr/tests/test_api.py
T
Vladimir BryzgalovandClaude Opus 5 9dc67bda5c talkscore-asr 0.1.0: сервис транскрибации и диаризации
Локальный FastAPI-сервис поверх GigaAM v3 и sherpa-onnx: приём аудио,
очередь задач, разделение по говорящим, постобработка терминов.

Доставка на Windows - ZIP со встроенным Python, без установки чего-либо.
Обновление кода при запуске тянется из релизов Gitea: меняется только
папка app, десятки килобайт вместо всего пакета.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 21:41:04 +05:00

125 lines
4.8 KiB
Python

"""Тесты HTTP-слоя. Пайплайн подменяется заглушкой: модели тут не нужны."""
import sys
import types
import pytest
from fastapi.testclient import TestClient
TOKEN = "test-token-123"
@pytest.fixture
def client(tmp_path, monkeypatch):
"""Поднимает приложение с временным каталогом и заглушкой моделей."""
config = tmp_path / "config.toml"
config.write_text(
f'[server]\nhost="127.0.0.1"\nport=8756\n'
f'[security]\ntoken="{TOKEN}"\nallow_ips=""\n'
f'[processing]\nthreads=1\nspeakers=2\nmax_upload_mb=1\n',
encoding="utf-8",
)
monkeypatch.setenv("TALKSCORE_ASR_HOME", str(tmp_path))
# sherpa_onnx и onnx_asr в тестах не нужны - подменяем до импорта приложения
for name in ("sherpa_onnx", "onnx_asr", "onnxruntime"):
monkeypatch.setitem(sys.modules, name, types.ModuleType(name))
for mod in [m for m in list(sys.modules) if m.startswith("app.")]:
del sys.modules[mod]
import app.config as cfg
monkeypatch.setattr(cfg, "BASE_DIR", tmp_path)
import app.main as main
monkeypatch.setattr(main.pipeline, "ffmpeg", "/bin/true", raising=False)
main._state["ready"] = True
main._worker_stop.set() # воркер не должен разбирать очередь в тестах
with TestClient(main.app) as c:
c.headers.update({"Authorization": f"Bearer {TOKEN}"})
yield c
class TestAuth:
def test_health_needs_no_token(self, client):
client.headers.pop("Authorization")
assert client.get("/health").status_code == 200
def test_jobs_require_token(self, client):
client.headers.pop("Authorization")
r = client.post("/v1/jobs", files={"file": ("a.wav", b"x")})
assert r.status_code == 401
def test_wrong_token_rejected(self, client):
client.headers.update({"Authorization": "Bearer nope"})
r = client.get("/v1/jobs/whatever")
assert r.status_code == 401
def test_valid_token_passes(self, client):
assert client.get("/v1/jobs/nonexistent").status_code == 404
class TestUpload:
def test_creates_job(self, client):
r = client.post("/v1/jobs", files={"file": ("call.mp3", b"fake audio data")})
assert r.status_code == 200
body = r.json()
assert body["status"] == "queued"
assert len(body["job_id"]) == 32
def test_rejects_empty_file(self, client):
r = client.post("/v1/jobs", files={"file": ("empty.wav", b"")})
assert r.status_code == 400
def test_rejects_too_large_file(self, client):
"""Лимит в тестовом конфиге - 1 МБ."""
r = client.post("/v1/jobs", files={"file": ("big.wav", b"x" * (2 * 1024 * 1024))})
assert r.status_code == 413
def test_speakers_parameter_is_stored(self, client):
r = client.post("/v1/jobs?speakers=3", files={"file": ("a.wav", b"data")})
assert r.status_code == 200
def test_speakers_out_of_range_rejected(self, client):
r = client.post("/v1/jobs?speakers=99", files={"file": ("a.wav", b"data")})
assert r.status_code == 422
class TestJobStatus:
def test_unknown_job_is_404(self, client):
assert client.get("/v1/jobs/deadbeef").status_code == 404
def test_queued_job_reports_position(self, client):
job_id = client.post("/v1/jobs", files={"file": ("a.wav", b"data")}).json()["job_id"]
body = client.get(f"/v1/jobs/{job_id}").json()
assert body["status"] == "queued"
assert body["queue_position"] == 0
def test_second_job_is_behind_first(self, client):
client.post("/v1/jobs", files={"file": ("a.wav", b"data")})
second = client.post("/v1/jobs", files={"file": ("b.wav", b"data")}).json()["job_id"]
assert client.get(f"/v1/jobs/{second}").json()["queue_position"] == 1
def test_delete_removes_job(self, client):
job_id = client.post("/v1/jobs", files={"file": ("a.wav", b"data")}).json()["job_id"]
assert client.delete(f"/v1/jobs/{job_id}").status_code == 200
assert client.get(f"/v1/jobs/{job_id}").status_code == 404
class TestHealth:
def test_reports_queue_and_threads(self, client):
body = client.get("/health").json()
assert "queue" in body and "threads" in body
class TestSchemaExposure:
"""Схема API не требует токена, поэтому по умолчанию она закрыта."""
def test_docs_hidden_by_default(self, client):
assert client.get("/docs").status_code == 404
def test_openapi_hidden_by_default(self, client):
assert client.get("/openapi.json").status_code == 404
def test_redoc_hidden_by_default(self, client):
assert client.get("/redoc").status_code == 404