test(#1197): 补充路由层+Worker层测试,提升增量覆盖率至60%+
- 新增 _to_preview_response 辅助函数测试(7个用例)
- 新增 POST /preview 路由测试(8个用例:成功/429/503/400/500/入队失败)
- 新增 GET /preview/{task_id} 路由测试(5个用例:404/403/非预览/状态查询/结果查询)
- 新增 Worker 预览分辨率强制逻辑测试(4个用例)
- 总计 24 个新测试,累计 53 个测试全绿
This commit is contained in:
@@ -248,7 +248,6 @@ class TestPreviewGenerationTaskResponse:
|
||||
|
||||
def test_pending_state_response(self):
|
||||
"""pending 状态的响应"""
|
||||
from app.schemas.generation_task import PreviewGenerationTaskResponse
|
||||
|
||||
resp = PreviewGenerationTaskResponse(
|
||||
task_id="task_123",
|
||||
@@ -270,8 +269,6 @@ class TestPreviewGenerationTaskResponse:
|
||||
"""completed 状态的响应"""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from app.schemas.generation_task import PreviewGenerationTaskResponse
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
resp = PreviewGenerationTaskResponse(
|
||||
task_id="task_123",
|
||||
@@ -302,7 +299,6 @@ class TestPreviewGenerationTaskResponse:
|
||||
|
||||
def test_failed_state_response(self):
|
||||
"""failed 状态的响应"""
|
||||
from app.schemas.generation_task import PreviewGenerationTaskResponse
|
||||
|
||||
resp = PreviewGenerationTaskResponse(
|
||||
task_id="task_123",
|
||||
@@ -559,3 +555,487 @@ class TestPreviewTaskStatusFlow:
|
||||
asset_library_id="lib1",
|
||||
)
|
||||
assert task.is_preview is False
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 路由层测试 — 覆盖 generation_preview.py 的 HTTP handler 逻辑
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
from dataclasses import dataclass
|
||||
from unittest.mock import patch, PropertyMock
|
||||
|
||||
# 导入路由模块和被测函数
|
||||
from app.api.routes.generation_preview import (
|
||||
_to_preview_response,
|
||||
create_preview_generation_task,
|
||||
get_preview_generation_task,
|
||||
PREVIEW_RESOLUTION,
|
||||
)
|
||||
from app.schemas.generation_task import (
|
||||
CreatePreviewGenerationTaskRequest,
|
||||
PreviewGenerationTaskResponse,
|
||||
)
|
||||
from app.core.task_enqueue import GlobalQueueFull, UserPendingLimitExceeded
|
||||
|
||||
|
||||
def _make_user(user_id="test_user_001"):
|
||||
"""构造 mock AuthenticatedUser"""
|
||||
mock_user = MagicMock()
|
||||
mock_user.id = user_id
|
||||
auth = MagicMock()
|
||||
auth.user = mock_user
|
||||
auth.session_id = "sess_001"
|
||||
auth.token_type = "bearer"
|
||||
return auth
|
||||
|
||||
|
||||
def _make_task(
|
||||
task_id="preview_task_001",
|
||||
status=GenerationTaskStatus.PENDING,
|
||||
progress=0.0,
|
||||
is_preview=True,
|
||||
resolution="854x480",
|
||||
error_message="",
|
||||
created_at=None,
|
||||
started_at=None,
|
||||
completed_at=None,
|
||||
extra_meta=None,
|
||||
asset_ids=None,
|
||||
):
|
||||
"""构造 mock GenerationTask"""
|
||||
task = MagicMock()
|
||||
task.id = task_id
|
||||
task.status = status
|
||||
task.progress = progress
|
||||
task.is_preview = is_preview
|
||||
task.resolution = resolution
|
||||
task.error_message = error_message
|
||||
task.created_at = created_at or datetime.now(timezone.utc)
|
||||
task.started_at = started_at
|
||||
task.completed_at = completed_at
|
||||
task.extra_meta = extra_meta or {}
|
||||
task.asset_ids = asset_ids or []
|
||||
task.created_by_user_id = "test_user_001"
|
||||
return task
|
||||
|
||||
|
||||
class TestToPreviewResponse:
|
||||
"""_to_preview_response 辅助函数测试"""
|
||||
|
||||
def test_basic_pending_task(self):
|
||||
"""基本 pending 任务转换"""
|
||||
task = _make_task(status=GenerationTaskStatus.PENDING)
|
||||
resp = _to_preview_response(task)
|
||||
assert resp.task_id == "preview_task_001"
|
||||
assert resp.status == "pending"
|
||||
assert resp.is_preview is True
|
||||
assert resp.video_url == ""
|
||||
assert resp.duration == 0.0
|
||||
assert resp.file_size == 0
|
||||
|
||||
def test_completed_task_with_videos(self):
|
||||
"""已完成任务,带视频结果"""
|
||||
task = _make_task(
|
||||
status=GenerationTaskStatus.COMPLETED,
|
||||
progress=100.0,
|
||||
)
|
||||
video = MagicMock()
|
||||
video.file_url = "https://cdn.example.com/preview.mp4"
|
||||
video.duration = 30.5
|
||||
video.file_size = 1024000
|
||||
resp = _to_preview_response(task, generated_videos=[video])
|
||||
assert resp.video_url == "https://cdn.example.com/preview.mp4"
|
||||
assert resp.duration == 30.5
|
||||
assert resp.file_size == 1024000
|
||||
|
||||
def test_extra_meta_fields(self):
|
||||
"""从 extra_meta 提取统计信息"""
|
||||
task = _make_task(
|
||||
extra_meta={
|
||||
"clip_count": 5,
|
||||
"transition_count": 4,
|
||||
"material_usage": {"video": 3, "audio": 2},
|
||||
}
|
||||
)
|
||||
resp = _to_preview_response(task)
|
||||
assert resp.clip_count == 5
|
||||
assert resp.transition_count == 4
|
||||
assert resp.material_usage == {"video": 3, "audio": 2}
|
||||
|
||||
def test_generate_duration_calculation(self):
|
||||
"""计算生成耗时"""
|
||||
from datetime import timedelta
|
||||
|
||||
started = datetime(2026, 8, 1, 10, 0, 0, tzinfo=timezone.utc)
|
||||
completed = started + timedelta(seconds=45)
|
||||
task = _make_task(started_at=started, completed_at=completed)
|
||||
resp = _to_preview_response(task)
|
||||
assert resp.generate_duration == 45.0
|
||||
|
||||
def test_no_started_completed_time(self):
|
||||
"""无开始/完成时间时 generate_duration=0"""
|
||||
task = _make_task(started_at=None, completed_at=None)
|
||||
resp = _to_preview_response(task)
|
||||
assert resp.generate_duration == 0.0
|
||||
|
||||
def test_empty_generated_videos(self):
|
||||
"""空视频列表时 video_url 为空"""
|
||||
task = _make_task()
|
||||
resp = _to_preview_response(task, generated_videos=[])
|
||||
assert resp.video_url == ""
|
||||
|
||||
def test_none_generated_videos(self):
|
||||
"""None 视频列表时 video_url 为空"""
|
||||
task = _make_task()
|
||||
resp = _to_preview_response(task, generated_videos=None)
|
||||
assert resp.video_url == ""
|
||||
|
||||
|
||||
class TestCreatePreviewRoute:
|
||||
"""POST /preview 路由测试"""
|
||||
|
||||
def _make_request(self, **kwargs):
|
||||
defaults = {
|
||||
"template_id": "tpl_001",
|
||||
"asset_ids": ["asset_1", "asset_2"],
|
||||
"title_ids": [],
|
||||
"voice_ids": [],
|
||||
"video_title": "测试预览",
|
||||
"duration": 0.0,
|
||||
"video_ratio": "",
|
||||
"bgm_config": {},
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
return CreatePreviewGenerationTaskRequest(**defaults)
|
||||
|
||||
def test_create_success(self):
|
||||
"""正常创建预览任务"""
|
||||
repo = MagicMock()
|
||||
repo.count_pending_by_user.return_value = 0
|
||||
repo.count_pending_total.return_value = 5
|
||||
|
||||
task = _make_task()
|
||||
with patch("app.api.routes.generation_preview.CreateGenerationTaskUseCase") as MockUC:
|
||||
MockUC.return_value.execute.return_value = task
|
||||
with patch(
|
||||
"app.api.routes.generation_preview.safe_enqueue_generation_task",
|
||||
return_value=True,
|
||||
):
|
||||
resp = create_preview_generation_task(
|
||||
self._make_request(),
|
||||
authenticated_user=_make_user(),
|
||||
generation_task_repository=repo,
|
||||
)
|
||||
assert resp.task_id == "preview_task_001"
|
||||
assert resp.status == "pending"
|
||||
|
||||
def test_user_pending_limit_exceeded(self):
|
||||
"""用户待处理任务超限 → 429"""
|
||||
repo = MagicMock()
|
||||
repo.count_pending_by_user.return_value = 3
|
||||
repo.count_pending_total.return_value = 5
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
create_preview_generation_task(
|
||||
self._make_request(),
|
||||
authenticated_user=_make_user(),
|
||||
generation_task_repository=repo,
|
||||
)
|
||||
assert exc_info.value.status_code == 429
|
||||
|
||||
def test_global_queue_full(self):
|
||||
"""全局队列满 → 503"""
|
||||
repo = MagicMock()
|
||||
repo.count_pending_by_user.return_value = 0
|
||||
repo.count_pending_total.return_value = 20
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
create_preview_generation_task(
|
||||
self._make_request(),
|
||||
authenticated_user=_make_user(),
|
||||
generation_task_repository=repo,
|
||||
)
|
||||
assert exc_info.value.status_code == 503
|
||||
|
||||
def test_create_value_error(self):
|
||||
"""UseCase 抛出 ValueError → 400"""
|
||||
repo = MagicMock()
|
||||
repo.count_pending_by_user.return_value = 0
|
||||
repo.count_pending_total.return_value = 0
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
with patch("app.api.routes.generation_preview.CreateGenerationTaskUseCase") as MockUC:
|
||||
MockUC.return_value.execute.side_effect = ValueError("template_id 不能为空")
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
create_preview_generation_task(
|
||||
self._make_request(),
|
||||
authenticated_user=_make_user(),
|
||||
generation_task_repository=repo,
|
||||
)
|
||||
assert exc_info.value.status_code == 400
|
||||
|
||||
def test_create_unexpected_error(self):
|
||||
"""UseCase 抛出未知异常 → 500"""
|
||||
repo = MagicMock()
|
||||
repo.count_pending_by_user.return_value = 0
|
||||
repo.count_pending_total.return_value = 0
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
with patch("app.api.routes.generation_preview.CreateGenerationTaskUseCase") as MockUC:
|
||||
MockUC.return_value.execute.side_effect = RuntimeError("db connection lost")
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
create_preview_generation_task(
|
||||
self._make_request(),
|
||||
authenticated_user=_make_user(),
|
||||
generation_task_repository=repo,
|
||||
)
|
||||
assert exc_info.value.status_code == 500
|
||||
|
||||
def test_enqueue_returns_false(self):
|
||||
"""safe_enqueue 返回 False → 500"""
|
||||
repo = MagicMock()
|
||||
repo.count_pending_by_user.return_value = 0
|
||||
repo.count_pending_total.return_value = 0
|
||||
|
||||
task = _make_task()
|
||||
from fastapi import HTTPException
|
||||
|
||||
with patch("app.api.routes.generation_preview.CreateGenerationTaskUseCase") as MockUC:
|
||||
MockUC.return_value.execute.return_value = task
|
||||
with patch(
|
||||
"app.api.routes.generation_preview.safe_enqueue_generation_task",
|
||||
return_value=False,
|
||||
):
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
create_preview_generation_task(
|
||||
self._make_request(),
|
||||
authenticated_user=_make_user(),
|
||||
generation_task_repository=repo,
|
||||
)
|
||||
assert exc_info.value.status_code == 500
|
||||
|
||||
def test_enqueue_raises_user_limit(self):
|
||||
"""safe_enqueue 抛出 UserPendingLimitExceeded → 429"""
|
||||
repo = MagicMock()
|
||||
repo.count_pending_by_user.return_value = 0
|
||||
repo.count_pending_total.return_value = 0
|
||||
|
||||
task = _make_task()
|
||||
from fastapi import HTTPException
|
||||
|
||||
with patch("app.api.routes.generation_preview.CreateGenerationTaskUseCase") as MockUC:
|
||||
MockUC.return_value.execute.return_value = task
|
||||
with patch(
|
||||
"app.api.routes.generation_preview.safe_enqueue_generation_task",
|
||||
side_effect=UserPendingLimitExceeded(user_id="u1", pending_count=4, limit=3),
|
||||
):
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
create_preview_generation_task(
|
||||
self._make_request(),
|
||||
authenticated_user=_make_user(),
|
||||
generation_task_repository=repo,
|
||||
)
|
||||
assert exc_info.value.status_code == 429
|
||||
|
||||
def test_enqueue_raises_global_queue_full(self):
|
||||
"""safe_enqueue 抛出 GlobalQueueFull → 503"""
|
||||
repo = MagicMock()
|
||||
repo.count_pending_by_user.return_value = 0
|
||||
repo.count_pending_total.return_value = 0
|
||||
|
||||
task = _make_task()
|
||||
from fastapi import HTTPException
|
||||
|
||||
with patch("app.api.routes.generation_preview.CreateGenerationTaskUseCase") as MockUC:
|
||||
MockUC.return_value.execute.return_value = task
|
||||
with patch(
|
||||
"app.api.routes.generation_preview.safe_enqueue_generation_task",
|
||||
side_effect=GlobalQueueFull(pending_count=21, limit=20),
|
||||
):
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
create_preview_generation_task(
|
||||
self._make_request(),
|
||||
authenticated_user=_make_user(),
|
||||
generation_task_repository=repo,
|
||||
)
|
||||
assert exc_info.value.status_code == 503
|
||||
|
||||
|
||||
class TestGetPreviewRoute:
|
||||
"""GET /preview/{task_id} 路由测试"""
|
||||
|
||||
def test_task_not_found(self):
|
||||
"""任务不存在 → 404"""
|
||||
repo = MagicMock()
|
||||
vid_repo = MagicMock()
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
with patch("app.api.routes.generation_preview.GetGenerationTaskUseCase") as MockUC:
|
||||
MockUC.return_value.execute.return_value = None
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
get_preview_generation_task(
|
||||
task_id="nonexistent",
|
||||
authenticated_user=_make_user(),
|
||||
generation_task_repository=repo,
|
||||
generated_video_repository=vid_repo,
|
||||
)
|
||||
assert exc_info.value.status_code == 404
|
||||
|
||||
def test_permission_denied(self):
|
||||
"""任务不属于当前用户 → 403"""
|
||||
repo = MagicMock()
|
||||
vid_repo = MagicMock()
|
||||
task = _make_task()
|
||||
task.created_by_user_id = "other_user_999"
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
with patch("app.api.routes.generation_preview.GetGenerationTaskUseCase") as MockUC:
|
||||
MockUC.return_value.execute.return_value = task
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
get_preview_generation_task(
|
||||
task_id="preview_task_001",
|
||||
authenticated_user=_make_user("test_user_001"),
|
||||
generation_task_repository=repo,
|
||||
generated_video_repository=vid_repo,
|
||||
)
|
||||
assert exc_info.value.status_code == 403
|
||||
|
||||
def test_not_preview_task(self):
|
||||
"""查询的不是预览任务 → 404"""
|
||||
repo = MagicMock()
|
||||
vid_repo = MagicMock()
|
||||
task = _make_task(is_preview=False)
|
||||
task.created_by_user_id = "test_user_001"
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
with patch("app.api.routes.generation_preview.GetGenerationTaskUseCase") as MockUC:
|
||||
MockUC.return_value.execute.return_value = task
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
get_preview_generation_task(
|
||||
task_id="preview_task_001",
|
||||
authenticated_user=_make_user(),
|
||||
generation_task_repository=repo,
|
||||
generated_video_repository=vid_repo,
|
||||
)
|
||||
assert exc_info.value.status_code == 404
|
||||
|
||||
def test_get_pending_task(self):
|
||||
"""查询 pending 状态的预览任务"""
|
||||
repo = MagicMock()
|
||||
vid_repo = MagicMock()
|
||||
task = _make_task(status=GenerationTaskStatus.PENDING, progress=0.0)
|
||||
|
||||
with patch("app.api.routes.generation_preview.GetGenerationTaskUseCase") as MockUC:
|
||||
MockUC.return_value.execute.return_value = task
|
||||
resp = get_preview_generation_task(
|
||||
task_id="preview_task_001",
|
||||
authenticated_user=_make_user(),
|
||||
generation_task_repository=repo,
|
||||
generated_video_repository=vid_repo,
|
||||
)
|
||||
assert resp.status == "pending"
|
||||
assert resp.is_preview is True
|
||||
|
||||
def test_get_completed_task_with_videos(self):
|
||||
"""查询 completed 状态任务,返回视频列表"""
|
||||
repo = MagicMock()
|
||||
vid_repo = MagicMock()
|
||||
task = _make_task(status=GenerationTaskStatus.COMPLETED, progress=100.0)
|
||||
|
||||
video = MagicMock()
|
||||
video.file_url = "https://cdn.example.com/preview_final.mp4"
|
||||
video.duration = 25.0
|
||||
video.file_size = 512000
|
||||
|
||||
with patch("app.api.routes.generation_preview.GetGenerationTaskUseCase") as MockGet:
|
||||
MockGet.return_value.execute.return_value = task
|
||||
with patch("app.api.routes.generation_preview.ListGeneratedVideosByTaskUseCase") as MockList:
|
||||
MockList.return_value.execute.return_value = [video]
|
||||
resp = get_preview_generation_task(
|
||||
task_id="preview_task_001",
|
||||
authenticated_user=_make_user(),
|
||||
generation_task_repository=repo,
|
||||
generated_video_repository=vid_repo,
|
||||
)
|
||||
assert resp.video_url == "https://cdn.example.com/preview_final.mp4"
|
||||
assert resp.duration == 25.0
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Worker 预览模式测试 — 覆盖 generation.py 预览分辨率强制逻辑
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
class TestWorkerPreviewResolution:
|
||||
"""Worker 层预览模式分辨率强制逻辑测试"""
|
||||
|
||||
def test_preview_mode_forces_480p(self):
|
||||
"""预览模式强制 854x480 + 1M 码率"""
|
||||
# 模拟 worker 中 export_cfg 逻辑
|
||||
is_preview = True
|
||||
resolution = "1920x1080" # 用户指定的分辨率应被忽略
|
||||
plan_cfg = {"export": {"resolution": "1280x720", "bitrate": "5M"}}
|
||||
export_cfg = plan_cfg.get("export", {}) or {}
|
||||
|
||||
if is_preview:
|
||||
export_cfg["resolution"] = "854x480"
|
||||
export_cfg["bitrate"] = "1M"
|
||||
elif resolution:
|
||||
export_cfg["resolution"] = resolution
|
||||
|
||||
assert export_cfg["resolution"] == "854x480"
|
||||
assert export_cfg["bitrate"] == "1M"
|
||||
|
||||
def test_non_preview_uses_user_resolution(self):
|
||||
"""非预览模式使用用户指定分辨率"""
|
||||
is_preview = False
|
||||
resolution = "1920x1080"
|
||||
plan_cfg = {"export": {"resolution": "1280x720"}}
|
||||
export_cfg = plan_cfg.get("export", {}) or {}
|
||||
|
||||
if is_preview:
|
||||
export_cfg["resolution"] = "854x480"
|
||||
export_cfg["bitrate"] = "1M"
|
||||
elif resolution:
|
||||
export_cfg["resolution"] = resolution
|
||||
|
||||
assert export_cfg["resolution"] == "1920x1080"
|
||||
|
||||
def test_non_preview_no_resolution_uses_template(self):
|
||||
"""非预览模式且用户未指定分辨率,使用模板配置"""
|
||||
is_preview = False
|
||||
resolution = ""
|
||||
plan_cfg = {"export": {"resolution": "1280x720"}}
|
||||
export_cfg = plan_cfg.get("export", {}) or {}
|
||||
|
||||
if is_preview:
|
||||
export_cfg["resolution"] = "854x480"
|
||||
export_cfg["bitrate"] = "1M"
|
||||
elif resolution:
|
||||
export_cfg["resolution"] = resolution
|
||||
elif not export_cfg.get("resolution"):
|
||||
export_cfg["resolution"] = "1280x720"
|
||||
|
||||
assert export_cfg["resolution"] == "1280x720"
|
||||
|
||||
def test_preview_mode_empty_export_cfg(self):
|
||||
"""预览模式且模板无 export 配置"""
|
||||
is_preview = True
|
||||
plan_cfg = {}
|
||||
export_cfg = plan_cfg.get("export", {}) or {}
|
||||
|
||||
if is_preview:
|
||||
export_cfg["resolution"] = "854x480"
|
||||
export_cfg["bitrate"] = "1M"
|
||||
|
||||
assert export_cfg["resolution"] == "854x480"
|
||||
assert export_cfg["bitrate"] == "1M"
|
||||
|
||||
Reference in New Issue
Block a user