cd7e75a845
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (push) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / PR Build API Image (push) Has been cancelled
CI/CD Pipeline / PR Build Web Image (push) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
608 lines
24 KiB
Python
Executable File
608 lines
24 KiB
Python
Executable File
"""P0/P1 修复单元测试 — 一键生成 P0 问题 + P1 校验.
|
||
|
||
覆盖:
|
||
P0-1: _download_library_assets 双模式查询(asset_library_id / project_id)
|
||
P0-2: OSS 上传失败抛异常 + URL 可访问性校验
|
||
P0-3: FFmpeg 失败时完整 stderr 日志
|
||
P1: template_id 存在性校验 + asset_ids 归属校验
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
import subprocess
|
||
import sys
|
||
from pathlib import Path
|
||
from unittest.mock import MagicMock, patch
|
||
|
||
import pytest
|
||
|
||
# 添加 worker app 到 sys.path
|
||
_WORKER_ROOT = Path(__file__).resolve().parents[2] / "apps" / "worker"
|
||
if str(_WORKER_ROOT) not in sys.path:
|
||
sys.path.insert(0, str(_WORKER_ROOT))
|
||
|
||
|
||
# ── P0-1: _download_library_assets ────────────────────────────────────────────
|
||
|
||
|
||
class TestDownloadLibraryAssets:
|
||
"""P0-1: 素材下载双模式 + 错误处理."""
|
||
|
||
def _make_asset(self, id_: str, file_url: str, project_id: str = "p1", library_id: str = "lib1"):
|
||
mock = MagicMock()
|
||
mock.id = id_
|
||
mock.file_url = file_url
|
||
mock.name = f"asset_{id_}"
|
||
mock.project_id = project_id
|
||
mock.asset_library_id = library_id
|
||
return mock
|
||
|
||
@patch("worker_app.tasks.generation.SessionLocal")
|
||
@patch("worker_app.tasks.generation.download_asset")
|
||
def test_asset_library_mode(self, mock_download, mock_session_factory):
|
||
"""素材库模式:按 asset_library_id 查询."""
|
||
from worker_app.tasks.generation import _download_library_assets
|
||
|
||
session = MagicMock()
|
||
mock_session_factory.return_value = session
|
||
query = MagicMock()
|
||
session.query.return_value = query
|
||
filter_result = MagicMock()
|
||
query.filter.return_value = filter_result
|
||
in_filter = MagicMock()
|
||
filter_result.filter.return_value = in_filter
|
||
assets = [self._make_asset("a1", "video/a1.mp4")]
|
||
in_filter.order_by.return_value.all.return_value = assets
|
||
|
||
mock_download.return_value = True
|
||
|
||
with patch("worker_app.tasks.generation.AssetModel", create=True):
|
||
result = _download_library_assets(
|
||
Path("/tmp/test"),
|
||
asset_library_id="lib1",
|
||
)
|
||
|
||
assert len(result) == 1
|
||
mock_download.assert_called_once()
|
||
|
||
@patch("worker_app.tasks.generation.SessionLocal")
|
||
@patch("worker_app.tasks.generation.download_asset")
|
||
def test_project_mode(self, mock_download, mock_session_factory):
|
||
"""项目级模式:asset_library_id 为空时按 project_id 查询."""
|
||
from worker_app.tasks.generation import _download_library_assets
|
||
|
||
session = MagicMock()
|
||
mock_session_factory.return_value = session
|
||
query = MagicMock()
|
||
session.query.return_value = query
|
||
filter_result = MagicMock()
|
||
query.filter.return_value = filter_result
|
||
proj_filter = MagicMock()
|
||
filter_result.filter.return_value = proj_filter
|
||
assets = [self._make_asset("a1", "video/a1.mp4", project_id="proj1")]
|
||
proj_filter.order_by.return_value.all.return_value = assets
|
||
|
||
mock_download.return_value = True
|
||
|
||
result = _download_library_assets(
|
||
Path("/tmp/test"),
|
||
project_id="proj1",
|
||
)
|
||
|
||
assert len(result) == 1
|
||
|
||
def test_both_empty_raises(self):
|
||
"""asset_library_id 和 project_id 都为空时抛 ValueError."""
|
||
from worker_app.tasks.generation import _download_library_assets
|
||
|
||
with pytest.raises(ValueError, match="至少需要提供一个"):
|
||
_download_library_assets(Path("/tmp/test"))
|
||
|
||
@patch("worker_app.tasks.generation.SessionLocal")
|
||
def test_no_assets_found_raises(self, mock_session_factory):
|
||
"""查不到素材时抛 RuntimeError."""
|
||
from worker_app.tasks.generation import _download_library_assets
|
||
|
||
session = MagicMock()
|
||
mock_session_factory.return_value = session
|
||
query = MagicMock()
|
||
session.query.return_value = query
|
||
filter_result = MagicMock()
|
||
query.filter.return_value = filter_result
|
||
in_filter = MagicMock()
|
||
filter_result.filter.return_value = in_filter
|
||
in_filter.order_by.return_value.all.return_value = []
|
||
|
||
with pytest.raises(RuntimeError, match="未找到视频素材"):
|
||
_download_library_assets(
|
||
Path("/tmp/test"),
|
||
asset_library_id="lib1",
|
||
)
|
||
|
||
@patch("worker_app.tasks.generation.SessionLocal")
|
||
@patch("worker_app.tasks.generation.download_asset")
|
||
def test_all_asset_ids_fail_raises(self, mock_download, mock_session_factory):
|
||
"""指定 asset_ids 但全部下载失败时抛 RuntimeError."""
|
||
from worker_app.tasks.generation import _download_library_assets
|
||
|
||
session = MagicMock()
|
||
mock_session_factory.return_value = session
|
||
query = MagicMock()
|
||
session.query.return_value = query
|
||
filter_result = MagicMock()
|
||
query.filter.return_value = filter_result
|
||
id_filter = MagicMock()
|
||
filter_result.filter.return_value = id_filter
|
||
assets = [self._make_asset("a1", "video/a1.mp4")]
|
||
id_filter.order_by.return_value.all.return_value = assets
|
||
|
||
mock_download.return_value = False # 全部下载失败
|
||
|
||
with pytest.raises(RuntimeError, match="素材下载失败"):
|
||
_download_library_assets(
|
||
Path("/tmp/test"),
|
||
asset_library_id="lib1",
|
||
asset_ids=["a1"],
|
||
)
|
||
|
||
|
||
# ── P0-2: OSS 上传 + URL 校验 ────────────────────────────────────────────────
|
||
|
||
|
||
class TestOSSUploadAndVerify:
|
||
"""P0-2: OSS 上传失败抛异常 + URL 可访问性校验."""
|
||
|
||
def test_verify_url_accessible_success(self):
|
||
"""URL 可访问时返回 True."""
|
||
from worker_app.tasks.generation import _verify_url_accessible
|
||
|
||
mock_response = MagicMock()
|
||
mock_response.status = 200
|
||
mock_response.__enter__ = MagicMock(return_value=mock_response)
|
||
mock_response.__exit__ = MagicMock(return_value=False)
|
||
|
||
with patch("urllib.request.OpenerDirector.open", return_value=mock_response):
|
||
assert _verify_url_accessible("https://example.com/test.mp4") is True
|
||
|
||
def test_verify_url_accessible_failure(self):
|
||
"""URL 不可访问时返回 False."""
|
||
from worker_app.tasks.generation import _verify_url_accessible
|
||
|
||
with patch("urllib.request.OpenerDirector.open", side_effect=Exception("connection refused")):
|
||
assert _verify_url_accessible("https://example.com/test.mp4") is False
|
||
|
||
def test_verify_url_404(self):
|
||
"""URL 返回 404 时返回 False."""
|
||
from worker_app.tasks.generation import _verify_url_accessible
|
||
|
||
mock_response = MagicMock()
|
||
mock_response.status = 404
|
||
mock_response.__enter__ = MagicMock(return_value=mock_response)
|
||
mock_response.__exit__ = MagicMock(return_value=False)
|
||
|
||
with patch("urllib.request.OpenerDirector.open", return_value=mock_response):
|
||
assert _verify_url_accessible("https://example.com/test.mp4") is False
|
||
|
||
|
||
# ── P0-3: FFmpeg stderr 日志 ─────────────────────────────────────────────────
|
||
|
||
|
||
class TestFFmpegStderrLogging:
|
||
"""P0-3: FFmpeg 失败时完整 stderr 打到日志."""
|
||
|
||
def test_run_ffmpeg_logs_stderr_on_failure(self, caplog):
|
||
"""run_ffmpeg 失败时记录 stderr 到日志."""
|
||
from video_processing.ffmpeg_utils import run_ffmpeg
|
||
|
||
error = subprocess.CalledProcessError(
|
||
returncode=183,
|
||
cmd=["ffmpeg", "-y", "-i", "input.mp4", "output.mp4"],
|
||
output="",
|
||
stderr="Error message from ffmpeg: filter graph error details here",
|
||
)
|
||
|
||
with patch("subprocess.run", side_effect=error):
|
||
with caplog.at_level(logging.ERROR):
|
||
with pytest.raises(subprocess.CalledProcessError):
|
||
run_ffmpeg(["ffmpeg", "-y", "-i", "input.mp4", "output.mp4"])
|
||
|
||
assert "FFmpeg 命令失败" in caplog.text
|
||
assert "exit_code=183" in caplog.text
|
||
assert "filter graph error" in caplog.text
|
||
|
||
def test_run_ffmpeg_success(self):
|
||
"""run_ffmpeg 成功时正常返回."""
|
||
from video_processing.ffmpeg_utils import run_ffmpeg
|
||
|
||
mock_result = MagicMock()
|
||
mock_result.stdout = "output"
|
||
mock_result.stderr = ""
|
||
|
||
with patch("subprocess.run", return_value=mock_result):
|
||
stdout, stderr = run_ffmpeg(["ffmpeg", "-version"])
|
||
assert stdout == "output"
|
||
assert stderr == ""
|
||
|
||
|
||
# ── P1: template_id + asset_ids 校验 ─────────────────────────────────────────
|
||
|
||
|
||
class TestP1Validations:
|
||
"""P1: template_id 存在性校验 + asset_ids 归属校验."""
|
||
|
||
def test_validate_template_exists_success(self):
|
||
"""模板存在时不抛异常."""
|
||
from worker_app.tasks.generation import _validate_template_exists
|
||
|
||
mock_template = MagicMock()
|
||
mock_template.id = "tmpl_001"
|
||
mock_template.name = "Test Template"
|
||
mock_template.is_active = True
|
||
mock_template.status = "active"
|
||
mock_template.version = 1
|
||
|
||
session = MagicMock()
|
||
|
||
# EditTemplateModel 查询返回 None(走旧模板系统 fallback)
|
||
edit_query = MagicMock()
|
||
edit_filter = MagicMock()
|
||
edit_query.filter.return_value = edit_filter
|
||
edit_filter.first.return_value = None
|
||
|
||
# TemplateModel 查询返回 mock_template
|
||
old_query = MagicMock()
|
||
old_filter = MagicMock()
|
||
old_query.filter.return_value = old_filter
|
||
old_filter.first.return_value = mock_template
|
||
|
||
def _query_side_effect(model):
|
||
# 根据 model 类型返回不同的 query mock
|
||
name = getattr(model, "__name__", "")
|
||
if "EditTemplate" in name:
|
||
return edit_query
|
||
return old_query
|
||
|
||
session.query.side_effect = _query_side_effect
|
||
|
||
with patch("worker_app.tasks.generation.SessionLocal", return_value=session):
|
||
_validate_template_exists("tmpl_001") # 不抛异常
|
||
|
||
def test_validate_template_exists_not_found(self):
|
||
"""模板不存在时抛 ValueError."""
|
||
from worker_app.tasks.generation import _validate_template_exists
|
||
|
||
session = MagicMock()
|
||
|
||
# 两个系统查询都返回 None
|
||
def _query_side_effect(model):
|
||
q = MagicMock()
|
||
f = MagicMock()
|
||
q.filter.return_value = f
|
||
f.first.return_value = None
|
||
return q
|
||
|
||
session.query.side_effect = _query_side_effect
|
||
|
||
with patch("worker_app.tasks.generation.SessionLocal", return_value=session):
|
||
with pytest.raises(ValueError, match="模板不存在"):
|
||
_validate_template_exists("tmpl_nonexistent")
|
||
|
||
|
||
# ── P1: 一键生成 clip 级效果层映射 ───────────────────────────────────────────
|
||
|
||
|
||
class TestTemplateClipEffectMapping:
|
||
"""P1: 模板 clip 级效果层映射到一键生成素材 clips."""
|
||
|
||
def _make_virtual_clip(self, idx: int, clip_type: str = "main", config: dict | None = None):
|
||
from dataclasses import dataclass, field
|
||
|
||
_clip_type_val = clip_type
|
||
|
||
@dataclass
|
||
class FakeClip:
|
||
id: str = f"vc_{idx:03d}"
|
||
plan_id: str = "task_001"
|
||
clip_type: str = _clip_type_val
|
||
order: int = idx
|
||
asset_id: str = f"asset_{idx}"
|
||
duration: float = 5.0
|
||
transition_effect: str = "cut"
|
||
transition_duration: float = 0.0
|
||
playback_speed: float = 1.0
|
||
config: dict = field(default_factory=dict)
|
||
|
||
return FakeClip(config=config or {})
|
||
|
||
def _make_template_clip_config(self, clip_type: str = "main", transition: str = "cut", config: dict | None = None):
|
||
mock = MagicMock()
|
||
mock.clip_type = clip_type
|
||
mock.transition_effect = transition
|
||
mock.config = config or {}
|
||
mock.default_duration = 3.0
|
||
mock.text_template = ""
|
||
return mock
|
||
|
||
def test_transition_effect_mapped(self):
|
||
"""转场效果正确映射到素材 clips."""
|
||
from worker_app.tasks.generation import _apply_template_clip_effects
|
||
|
||
clips = [self._make_virtual_clip(i) for i in range(3)]
|
||
clip_configs = [
|
||
self._make_template_clip_config("main", transition="fade"),
|
||
self._make_template_clip_config("main", transition="dissolve"),
|
||
]
|
||
|
||
_apply_template_clip_effects(clips, clip_configs, "one_take")
|
||
|
||
# 前两个按顺序映射,第三个用最后一个模板配置
|
||
assert clips[0].transition_effect == "fade"
|
||
assert clips[1].transition_effect == "dissolve"
|
||
assert clips[2].transition_effect == "dissolve" # 复用最后一个
|
||
|
||
def test_color_grade_mapped(self):
|
||
"""滤镜配置正确映射到 clip.config.color_grade."""
|
||
from worker_app.tasks.generation import _apply_template_clip_effects
|
||
|
||
clips = [self._make_virtual_clip(i) for i in range(2)]
|
||
clip_configs = [
|
||
self._make_template_clip_config(
|
||
"main", config={"color_grade": {"enabled": True, "filter": "vintage", "brightness": 0.1}}
|
||
),
|
||
]
|
||
|
||
_apply_template_clip_effects(clips, clip_configs, "one_take")
|
||
|
||
assert clips[0].config["color_grade"]["filter"] == "vintage"
|
||
assert clips[0].config["color_grade"]["brightness"] == 0.1
|
||
# 第二个素材复用第一个模板配置
|
||
assert clips[1].config["color_grade"]["filter"] == "vintage"
|
||
|
||
def test_existing_config_preserved(self):
|
||
"""已有 clip.config 内容(如 role)被保留."""
|
||
from worker_app.tasks.generation import _apply_template_clip_effects
|
||
|
||
clips = [self._make_virtual_clip(0, config={"role": "b_roll"})]
|
||
clip_configs = [
|
||
self._make_template_clip_config("main", config={"color_grade": {"enabled": True, "filter": "warm"}}),
|
||
]
|
||
|
||
_apply_template_clip_effects(clips, clip_configs, "voice_over")
|
||
|
||
assert clips[0].config["role"] == "b_roll" # 保留原有配置
|
||
assert clips[0].config["color_grade"]["filter"] == "warm" # 新增滤镜配置
|
||
|
||
def test_empty_clip_configs_no_change(self):
|
||
"""空模板配置时 clips 保持不变."""
|
||
from worker_app.tasks.generation import _apply_template_clip_effects
|
||
|
||
clips = [self._make_virtual_clip(i) for i in range(2)]
|
||
_apply_template_clip_effects(clips, [], "one_take")
|
||
|
||
assert clips[0].transition_effect == "cut"
|
||
assert clips[1].transition_effect == "cut"
|
||
|
||
def test_cut_transition_not_overwritten(self):
|
||
"""模板转场为 cut 时不覆盖(保持默认)."""
|
||
from worker_app.tasks.generation import _apply_template_clip_effects
|
||
|
||
clips = [self._make_virtual_clip(0)]
|
||
clips[0].transition_effect = "fade" # 已有非默认值
|
||
clip_configs = [
|
||
self._make_template_clip_config("main", transition="cut"),
|
||
]
|
||
|
||
_apply_template_clip_effects(clips, clip_configs, "one_take")
|
||
|
||
# 模板是 cut 时,保留原有值(避免无意义覆盖)
|
||
assert clips[0].transition_effect == "fade"
|
||
|
||
def test_transition_duration_mapped(self):
|
||
"""转场时长(transition_duration)从模板 config 正确映射到 clip."""
|
||
from worker_app.tasks.generation import _apply_template_clip_effects
|
||
|
||
clips = [self._make_virtual_clip(i) for i in range(3)]
|
||
clip_configs = [
|
||
self._make_template_clip_config("main", transition="fade", config={"transition_duration": 0.8}),
|
||
self._make_template_clip_config("main", transition="dissolve", config={"transition_duration": 1.2}),
|
||
]
|
||
|
||
_apply_template_clip_effects(clips, clip_configs, "one_take")
|
||
|
||
# 前两个按顺序映射,第三个复用最后一个
|
||
assert clips[0].transition_effect == "fade"
|
||
assert clips[0].transition_duration == 0.8
|
||
assert clips[1].transition_effect == "dissolve"
|
||
assert clips[1].transition_duration == 1.2
|
||
assert clips[2].transition_effect == "dissolve"
|
||
assert clips[2].transition_duration == 1.2
|
||
|
||
def test_transition_duration_ignored_for_cut(self):
|
||
"""模板转场为 cut 时,transition_duration 不生效(保持默认0)."""
|
||
from worker_app.tasks.generation import _apply_template_clip_effects
|
||
|
||
clips = [self._make_virtual_clip(0)]
|
||
clip_configs = [
|
||
self._make_template_clip_config("main", transition="cut", config={"transition_duration": 0.5}),
|
||
]
|
||
|
||
_apply_template_clip_effects(clips, clip_configs, "one_take")
|
||
|
||
# cut 转场不映射,transition_duration 也不应用
|
||
assert clips[0].transition_duration == 0.0
|
||
|
||
def test_transition_duration_invalid_value_skipped(self):
|
||
"""transition_duration 为无效值时安全跳过."""
|
||
from worker_app.tasks.generation import _apply_template_clip_effects
|
||
|
||
clips = [self._make_virtual_clip(0)]
|
||
clip_configs = [
|
||
self._make_template_clip_config("main", transition="fade", config={"transition_duration": "abc"}),
|
||
]
|
||
|
||
_apply_template_clip_effects(clips, clip_configs, "one_take")
|
||
|
||
assert clips[0].transition_effect == "fade"
|
||
assert clips[0].transition_duration == 0.0 # 无效值保持默认
|
||
|
||
def test_intro_outro_extracted(self):
|
||
"""intro/outro 类型 clip_config 正确提取为 plan 级 intro_outro 配置."""
|
||
from worker_app.tasks.generation import _extract_intro_outro_from_clip_configs
|
||
|
||
clip_configs = [
|
||
self._make_template_clip_config("intro", config={"intro_type": "text", "intro_text_color": "#ffffff"}),
|
||
self._make_template_clip_config("main"),
|
||
self._make_template_clip_config("outro", config={"outro_type": "follow", "outro_follow_text": "关注我们"}),
|
||
]
|
||
# 设置 intro/outro 的 text_template
|
||
clip_configs[0].text_template = "精彩视频"
|
||
clip_configs[0].default_duration = 2.5
|
||
|
||
result = _extract_intro_outro_from_clip_configs(clip_configs)
|
||
|
||
assert result["has_intro"] is True
|
||
assert result["intro_type"] == "text"
|
||
assert result["intro_text"] == "精彩视频"
|
||
assert result["intro_duration"] == 2.5
|
||
assert result["intro_text_color"] == "#ffffff"
|
||
assert result["has_outro"] is True
|
||
assert result["outro_type"] == "follow"
|
||
assert result["outro_follow_text"] == "关注我们"
|
||
|
||
def test_intro_outro_empty_when_none(self):
|
||
"""没有 intro/outro 时返回空 dict."""
|
||
from worker_app.tasks.generation import _extract_intro_outro_from_clip_configs
|
||
|
||
clip_configs = [
|
||
self._make_template_clip_config("main"),
|
||
self._make_template_clip_config("main"),
|
||
]
|
||
|
||
result = _extract_intro_outro_from_clip_configs(clip_configs)
|
||
assert result == {}
|
||
|
||
|
||
class TestTemplatePlanConfigLoading:
|
||
"""验证从模板加载 plan 级配置(BGM、字幕、标题)的逻辑。"""
|
||
|
||
def _mock_template(
|
||
self,
|
||
title_config=None,
|
||
subtitle_config=None,
|
||
bgm_config=None,
|
||
is_active=True,
|
||
):
|
||
template = MagicMock()
|
||
template.id = "tmpl_001"
|
||
template.name = "Test Template"
|
||
template.is_active = is_active
|
||
template.title_config = title_config or {}
|
||
template.subtitle_config = subtitle_config or {}
|
||
template.bgm_config = bgm_config or {}
|
||
return template
|
||
|
||
def _mock_session(self, template):
|
||
session = MagicMock()
|
||
|
||
# EditTemplateModel 查询返回 None(走旧模板系统 fallback)
|
||
edit_query = MagicMock()
|
||
edit_filter = MagicMock()
|
||
edit_query.filter.return_value = edit_filter
|
||
edit_filter.first.return_value = None
|
||
|
||
# TemplateModel 查询返回 template(旧模板系统)
|
||
old_query = MagicMock()
|
||
old_filter = MagicMock()
|
||
old_query.filter.return_value = old_filter
|
||
old_filter.first.return_value = template
|
||
|
||
def _query_side_effect(model):
|
||
name = getattr(model, "__name__", "")
|
||
if "EditTemplate" in name:
|
||
return edit_query
|
||
return old_query
|
||
|
||
session.query.side_effect = _query_side_effect
|
||
return session
|
||
|
||
def test_load_template_config_assembles_three_fields(self):
|
||
"""模板的三个独立字段正确组装成 plan.config 格式。"""
|
||
from worker_app.tasks.generation import _load_template_plan_config
|
||
|
||
title_cfg = {"enabled": True, "text": "我的标题", "font_size": 36}
|
||
subtitle_cfg = {"enabled": True, "auto_generated": True, "language": "zh"}
|
||
bgm_cfg = {"enabled": True, "preset_id": "bgm-001", "volume": 0.5}
|
||
|
||
template = self._mock_template(
|
||
title_config=title_cfg,
|
||
subtitle_config=subtitle_cfg,
|
||
bgm_config=bgm_cfg,
|
||
)
|
||
session = self._mock_session(template)
|
||
|
||
with patch("worker_app.tasks.generation.SessionLocal", return_value=session):
|
||
result = _load_template_plan_config("tmpl_001")
|
||
|
||
assert result["title"] == title_cfg
|
||
assert result["subtitle"] == subtitle_cfg
|
||
assert result["bgm"] == bgm_cfg
|
||
|
||
def test_load_template_config_empty_template_returns_empty(self):
|
||
"""模板三个字段都为空时返回空 dict。"""
|
||
from worker_app.tasks.generation import _load_template_plan_config
|
||
|
||
template = self._mock_template()
|
||
session = self._mock_session(template)
|
||
|
||
with patch("worker_app.tasks.generation.SessionLocal", return_value=session):
|
||
result = _load_template_plan_config("tmpl_001")
|
||
|
||
assert result == {}
|
||
|
||
def test_load_template_config_only_bgm(self):
|
||
"""只有 BGM 配置时只返回 bgm 字段。"""
|
||
from worker_app.tasks.generation import _load_template_plan_config
|
||
|
||
bgm_cfg = {"enabled": True, "audio_url": "https://example.com/bgm.mp3"}
|
||
template = self._mock_template(bgm_config=bgm_cfg)
|
||
session = self._mock_session(template)
|
||
|
||
with patch("worker_app.tasks.generation.SessionLocal", return_value=session):
|
||
result = _load_template_plan_config("tmpl_001")
|
||
|
||
assert "bgm" in result
|
||
assert result["bgm"] == bgm_cfg
|
||
assert "title" not in result
|
||
assert "subtitle" not in result
|
||
|
||
def test_load_template_config_empty_template_id(self):
|
||
"""空 template_id 直接返回空 dict。"""
|
||
from worker_app.tasks.generation import _load_template_plan_config
|
||
|
||
result = _load_template_plan_config("")
|
||
assert result == {}
|
||
|
||
result = _load_template_plan_config(None)
|
||
assert result == {}
|
||
|
||
def test_load_template_config_not_found_returns_empty(self):
|
||
"""模板不存在时返回空 dict,不抛异常。"""
|
||
from worker_app.tasks.generation import _load_template_plan_config
|
||
|
||
session = MagicMock()
|
||
|
||
def _query_side_effect(model):
|
||
q = MagicMock()
|
||
f = MagicMock()
|
||
q.filter.return_value = f
|
||
f.first.return_value = None
|
||
return q
|
||
|
||
session.query.side_effect = _query_side_effect
|
||
|
||
with patch("worker_app.tasks.generation.SessionLocal", return_value=session):
|
||
result = _load_template_plan_config("tmpl_nonexist")
|
||
|
||
assert result == {}
|