"""lipsync 口型同步 — v1.6.2 起免费,不扣积分""" from __future__ import annotations import math from types import SimpleNamespace from unittest.mock import MagicMock, patch import pytest from fastapi import HTTPException def _cu(user_id="u1", is_member=False, member_type=None): cu = MagicMock() cu.user.id = user_id cu.user.is_member = is_member cu.user.member_type = member_type return cu def _body(**kw): b = MagicMock() defaults = dict( video_url="http://x/v.mp4", audio_url=None, audio_duration=None, sentence_timings=None, voice_id=None, script_text="你好世界", speed=1.0, emotion="", enable_video_loop=False, project_id=None, ) defaults.update(kw) for k, v in defaults.items(): setattr(b, k, v) return b class TestLipsyncFree: """lipsync 已移除手动扣点,业务异常仍按原状态码抛出。""" def test_ai_digital_human_returns_zero_cost(self): from packages.domain.points_rules import calculate_points_cost assert calculate_points_cost("ai_digital_human", is_member=False, duration_minutes=1) == 0 assert calculate_points_cost("ai_digital_human", is_member=True, duration_minutes=10) == 0 def test_module_has_no_points_imports(self): import inspect from app.api.routes import lipsync as mod src = inspect.getsource(mod) assert "PointsService" not in src assert "calculate_points_cost" not in src assert "_points_deducted" not in src assert "settings" not in src # settings was only used for points_enabled def test_docstring_at_top_of_create_lipsync_job(self): """扣点块删除后,docstring 必须在函数体第一行(防止函数体中段 docstring 丢失)。""" import ast import inspect from app.api.routes.lipsync import create_lipsync_job src = inspect.getsource(create_lipsync_job) tree = ast.parse(src) fn = tree.body[0] # docstring 应为函数体第一条语句 assert ( isinstance(fn.body[0], ast.Expr) and isinstance(fn.body[0].value, ast.Constant) and isinstance(fn.body[0].value.value, str) ), "create_lipsync_job docstring 不在函数体开头" def test_docstring_at_top_of_preview_tts(self): import ast import inspect from app.api.routes.lipsync import preview_tts src = inspect.getsource(preview_tts) tree = ast.parse(src) fn = tree.body[0] assert ( isinstance(fn.body[0], ast.Expr) and isinstance(fn.body[0].value, ast.Constant) and isinstance(fn.body[0].value.value, str) ), "preview_tts docstring 不在函数体开头" def test_value_error_still_raises_400(self): """业务异常仍抛 400(不再退费)。""" from app.api.routes.lipsync import create_lipsync_job db = MagicMock() svc = MagicMock() svc.create_job.side_effect = ValueError("bad input") with pytest.raises(HTTPException) as ei: create_lipsync_job(body=_body(), current_user=_cu(), db=db, svc=svc) assert ei.value.status_code == 400 def test_success_returns_job(self): from app.api.routes.lipsync import create_lipsync_job db = MagicMock() svc = MagicMock() job = SimpleNamespace(id="job-1", status="queued") svc.create_job.return_value = job # 不再依赖 settings/PointsService patch result = create_lipsync_job(body=_body(), current_user=_cu(), db=db, svc=svc) assert result is job