"""Тесты 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