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:
xiaoxia
2026-08-01 16:16:27 +08:00
committed by xiaoxia
parent 73af3851cb
commit bbddcb076a
+484 -4
View File
@@ -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"