Spaces:
Running
Running
| """Tests for batch dubbing API endpoints. | |
| These tests create a minimal FastAPI app with only the batch router, | |
| avoiding the heavy main app import chain. The batch module is | |
| lightweight — it only imports os, uuid, time, asyncio, logging, | |
| fastapi, and pydantic at module level. | |
| """ | |
| import io | |
| import os | |
| import sys | |
| import pytest | |
| # Add backend to path | |
| sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) | |
| # Stub core.config before batch imports it | |
| import types | |
| config_mod = types.ModuleType("core.config") | |
| config_mod.DATA_DIR = "/tmp/omnivoice_test_data" | |
| sys.modules["core.config"] = config_mod | |
| from fastapi import FastAPI | |
| from fastapi.testclient import TestClient | |
| from api.routers.batch import router, _jobs, _set_progress | |
| def reset_state(): | |
| """Clear in-memory state between tests and disable the worker.""" | |
| import api.routers.batch as batch | |
| batch._jobs.clear() | |
| batch._queue = None | |
| if batch._worker_task and not batch._worker_task.done(): | |
| batch._worker_task.cancel() | |
| batch._worker_task = None | |
| # Monkey-patch _ensure_queue to use a no-op worker so jobs stay queued | |
| original_ensure = batch._ensure_queue | |
| def _test_ensure_queue(): | |
| if batch._queue is None: | |
| import asyncio | |
| async def _noop(): | |
| while True: | |
| job_id = await batch._queue.get() | |
| batch._queue.task_done() | |
| batch._queue = asyncio.Queue() | |
| batch._worker_task = asyncio.ensure_future(_noop()) | |
| batch._ensure_queue = _test_ensure_queue | |
| yield | |
| batch._ensure_queue = original_ensure | |
| batch._jobs.clear() | |
| def client(): | |
| app = FastAPI() | |
| app.include_router(router) | |
| return TestClient(app) | |
| def fake_video(): | |
| return b"\x00\x00\x00\x1c\x66\x74\x79\x70" + b"\x00" * 1016 # 1KB | |
| def _enqueue(client, video_bytes, langs="es", voice_id="", preserve_bg="true"): | |
| return client.post( | |
| "/batch/enqueue", | |
| files={"video": ("test.mp4", io.BytesIO(video_bytes), "video/mp4")}, | |
| data={"langs": langs, "preserve_bg": preserve_bg, **({"voice_id": voice_id} if voice_id else {})}, | |
| ) | |
| class TestEnqueue: | |
| def test_returns_job_id(self, client, fake_video): | |
| resp = _enqueue(client, fake_video, "es,fr") | |
| assert resp.status_code == 200 | |
| body = resp.json() | |
| assert "job_id" in body | |
| assert body["status"] == "queued" | |
| def test_empty_langs_fails(self, client, fake_video): | |
| """Empty langs string should return 400.""" | |
| # Send with no langs field at all | |
| resp = client.post( | |
| "/batch/enqueue", | |
| files={"video": ("test.mp4", io.BytesIO(fake_video), "video/mp4")}, | |
| data={"langs": ",,,", "preserve_bg": "true"}, | |
| ) | |
| assert resp.status_code == 400 | |
| def test_multi_lang_splits(self, client, fake_video): | |
| resp = _enqueue(client, fake_video, "es,fr,de") | |
| job_id = resp.json()["job_id"] | |
| job = client.get(f"/batch/jobs/{job_id}").json() | |
| assert job["langs"] == ["es", "fr", "de"] | |
| def test_preserves_filename(self, client, fake_video): | |
| resp = _enqueue(client, fake_video) | |
| job_id = resp.json()["job_id"] | |
| job = client.get(f"/batch/jobs/{job_id}").json() | |
| assert job["filename"] == "test.mp4" | |
| class TestListJobs: | |
| def test_empty(self, client): | |
| resp = client.get("/batch/jobs") | |
| assert resp.status_code == 200 | |
| assert resp.json() == [] | |
| def test_returns_enqueued(self, client, fake_video): | |
| _enqueue(client, fake_video) | |
| _enqueue(client, fake_video) | |
| jobs = client.get("/batch/jobs").json() | |
| assert len(jobs) == 2 | |
| def test_filter_active(self, client, fake_video): | |
| r1 = _enqueue(client, fake_video).json() | |
| r2 = _enqueue(client, fake_video).json() | |
| client.post(f"/batch/jobs/{r2['job_id']}/cancel") | |
| active = client.get("/batch/jobs?status=active").json() | |
| assert len(active) == 1 | |
| assert active[0]["id"] == r1["job_id"] | |
| def test_filter_cancelled(self, client, fake_video): | |
| r = _enqueue(client, fake_video).json() | |
| client.post(f"/batch/jobs/{r['job_id']}/cancel") | |
| cancelled = client.get("/batch/jobs?status=cancelled").json() | |
| assert len(cancelled) == 1 | |
| class TestGetJob: | |
| def test_not_found(self, client): | |
| assert client.get("/batch/jobs/nope").status_code == 404 | |
| def test_found(self, client, fake_video): | |
| r = _enqueue(client, fake_video).json() | |
| job = client.get(f"/batch/jobs/{r['job_id']}").json() | |
| assert job["id"] == r["job_id"] | |
| assert job["status"] == "queued" | |
| class TestCancelJob: | |
| def test_cancel_queued(self, client, fake_video): | |
| r = _enqueue(client, fake_video).json() | |
| resp = client.post(f"/batch/jobs/{r['job_id']}/cancel") | |
| assert resp.json()["cancelled"] is True | |
| job = client.get(f"/batch/jobs/{r['job_id']}").json() | |
| assert job["status"] == "cancelled" | |
| def test_cancel_already_done(self, client, fake_video): | |
| r = _enqueue(client, fake_video).json() | |
| _jobs[r["job_id"]]["status"] = "done" | |
| resp = client.post(f"/batch/jobs/{r['job_id']}/cancel") | |
| assert resp.json()["already"] == "done" | |
| def test_cancel_not_found(self, client): | |
| assert client.post("/batch/jobs/nope/cancel").status_code == 404 | |
| class TestDeleteJob: | |
| def test_delete_cancelled(self, client, fake_video): | |
| r = _enqueue(client, fake_video).json() | |
| client.post(f"/batch/jobs/{r['job_id']}/cancel") | |
| resp = client.delete(f"/batch/jobs/{r['job_id']}") | |
| assert resp.json()["deleted"] is True | |
| assert client.get(f"/batch/jobs/{r['job_id']}").status_code == 404 | |
| def test_delete_not_found(self, client): | |
| assert client.delete("/batch/jobs/nope").status_code == 404 | |
| class TestSetProgress: | |
| def test_basic(self): | |
| job = {} | |
| _set_progress(job, "transcribe", 50, segments_count=10) | |
| assert job["progress"]["stage"] == "transcribe" | |
| assert job["progress"]["percent"] == 50 | |
| assert job["progress"]["segments_count"] == 10 | |
| def test_overwrite(self): | |
| job = {"progress": {"stage": "extract", "percent": 100}} | |
| _set_progress(job, "generate", 25, current_lang="es") | |
| assert job["progress"]["stage"] == "generate" | |
| assert job["progress"]["current_lang"] == "es" | |