blog2video-test / backend /tests /test_group6_background_dispatch.py
faisalnazeer67's picture
feat: add backend tests
aac16eb
Raw
History Blame Contribute Delete
5.42 kB
"""
Group 6 β€” Long-running / background-dispatch endpoints.
Tier A only checks that these endpoints ACCEPT the request, enforce their
guards, persist the job row, and register the background task with the right
arguments. It does NOT run the background work (regenerating voiceovers, calling
ElevenLabs/R2) β€” that is the depth tier.
To keep the work from actually executing under TestClient (which runs background
tasks synchronously after the response), ``BackgroundTasks.add_task`` is patched
to a spy that records calls without invoking them.
POST /api/projects/{id}/change-voice -> dispatches _run_voice_change
POST /api/projects/{id}/delete-voiceover -> dispatches _run_delete_voiceover
"""
import pytest
from starlette.background import BackgroundTasks
from app.models.project import Project, ProjectStatus
from app.models.project_voice_change_job import ProjectVoiceChangeJob
from app.models.scene import Scene
from app.services import voice_change_progress
pytestmark = pytest.mark.smoke
@pytest.fixture()
def captured_tasks(monkeypatch):
"""Record background tasks instead of running them. Returns the list of
(function_name, args) tuples registered during the test."""
recorded: list[tuple[str, tuple]] = []
def _spy(self, func, *args, **kwargs):
recorded.append((getattr(func, "__name__", str(func)), args))
monkeypatch.setattr(BackgroundTasks, "add_task", _spy, raising=True)
return recorded
@pytest.fixture(autouse=True)
def _reset_progress():
"""voice_change_progress keeps in-memory per-project state; rolled-back test
projects reuse ids, so clear it around each test to avoid a stale
'already in progress' 409 leaking across tests."""
voice_change_progress._progress.clear()
yield
voice_change_progress._progress.clear()
def _project_with_scenes(db, user, n=2) -> Project:
project = Project(
user_id=user.id, name="Has Scenes", blog_url="https://s.test",
status=ProjectStatus.GENERATED, voice_gender="female",
)
db.add(project)
db.commit()
db.refresh(project)
for i in range(n):
db.add(Scene(
project_id=project.id, order=i, title=f"Scene {i}",
narration_text="hello", visual_description="a visual",
))
db.commit()
return project
# ─── POST /api/projects/{id}/change-voice ───────────────────────────────────
def test_change_voice__valid__dispatches_and_creates_job(
client, db_session, paid_user, auth, captured_tasks
):
project = _project_with_scenes(db_session, paid_user)
resp = client.post(
f"/api/projects/{project.id}/change-voice",
headers=auth(paid_user),
json={"voice_gender": "male"},
)
assert resp.status_code == 200
assert resp.json() == {"started": True, "total": 2}
# A job row was persisted...
job = db_session.query(ProjectVoiceChangeJob).filter_by(project_id=project.id).first()
assert job is not None
# ...and the background worker was registered with (project_id, job_id).
names = [name for name, _ in captured_tasks]
assert "_run_voice_change" in names
args = next(a for n, a in captured_tasks if n == "_run_voice_change")
assert args[0] == project.id
def test_change_voice__other_user__returns_404(
client, db_session, paid_user, other_user, auth, captured_tasks
):
project = _project_with_scenes(db_session, paid_user)
resp = client.post(
f"/api/projects/{project.id}/change-voice",
headers=auth(other_user), json={"voice_gender": "male"},
)
assert resp.status_code == 404
def test_change_voice__no_scenes__returns_400(
client, db_session, paid_user, auth, captured_tasks
):
project = Project(user_id=paid_user.id, name="Empty", blog_url="https://e.test",
status=ProjectStatus.GENERATED, voice_gender="female")
db_session.add(project)
db_session.commit()
db_session.refresh(project)
resp = client.post(
f"/api/projects/{project.id}/change-voice",
headers=auth(paid_user), json={"voice_gender": "male"},
)
assert resp.status_code == 400
def test_change_voice__without_token__returns_401(client, db_session, paid_user):
project = _project_with_scenes(db_session, paid_user)
resp = client.post(f"/api/projects/{project.id}/change-voice", json={})
assert resp.status_code == 401
# ─── POST /api/projects/{id}/delete-voiceover ───────────────────────────────
def test_delete_voiceover__valid__dispatches_and_creates_job(
client, db_session, paid_user, auth, captured_tasks
):
project = _project_with_scenes(db_session, paid_user)
resp = client.post(
f"/api/projects/{project.id}/delete-voiceover", headers=auth(paid_user),
)
assert resp.status_code == 200
assert resp.json() == {"started": True, "total": 2}
names = [name for name, _ in captured_tasks]
assert "_run_delete_voiceover" in names
def test_delete_voiceover__other_user__returns_404(
client, db_session, paid_user, other_user, auth, captured_tasks
):
project = _project_with_scenes(db_session, paid_user)
resp = client.post(
f"/api/projects/{project.id}/delete-voiceover", headers=auth(other_user),
)
assert resp.status_code == 404