Files
xiaoxia-saas/tests/unit/test_template_use_cases.py
xiaoxia 5e74d0b565
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
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
test: wave216 template use cases +46单测(12个UC全量覆盖) (#1188)
2026-07-30 08:45:15 +08:00

712 lines
25 KiB
Python
Executable File

"""模板 Use Cases 单元测试 — wave216"""
from __future__ import annotations
from unittest.mock import MagicMock
import pytest
from packages.application.template.commands import (
CopyTemplateCommand,
CreateCategoryCommand,
CreateTemplateCommand,
ListTemplatesFilter,
SegmentCommand,
UpdateTemplateCommand,
ValidateTemplateCommand,
)
from packages.application.template.use_cases import (
CopyTemplateUseCase,
CountTemplatesUseCase,
CreateCategoryUseCase,
CreateTemplateUseCase,
DeleteCategoryUseCase,
DeleteTemplateUseCase,
GenerateWarning,
GetTemplateUsageUseCase,
GetTemplateUseCase,
ListCategoriesUseCase,
ListTagsUseCase,
ListTemplatesUseCase,
UpdateTemplateUseCase,
ValidateResult,
ValidateTemplateUseCase,
)
from packages.domain.editing_mode import EditingMode
from packages.domain.exceptions import NotFoundError, ValidationError
from packages.domain.template import Template, TemplateCategory, TemplateSegment
# ── helpers ──────────────────────────────────────────────────────────────────
def _make_template(
template_id="t1",
user_id="u1",
name="测试模板",
mode=EditingMode.ONE_TAKE.value,
category="",
estimated_duration=30.0,
segments=None,
):
if segments is None:
segments = [
TemplateSegment(
id="s1",
template_id=template_id,
segment_order=1,
duration_min=5.0,
duration_max=10.0,
)
]
return Template(
id=template_id,
user_id=user_id,
name=name,
mode=mode,
category=category,
estimated_duration=estimated_duration,
segments=segments,
)
def _make_segments(n, *, start_order=1, material_type=None):
return [
TemplateSegment(
id=f"s{i}",
template_id="t1",
segment_order=start_order + i - 1,
duration_min=5.0,
duration_max=10.0,
material_type=material_type,
)
for i in range(1, n + 1)
]
# ── CreateTemplateUseCase ────────────────────────────────────────────────────
class TestCreateTemplateUseCase:
def test_create_success(self):
repo = MagicMock()
repo.create.side_effect = lambda t: t
repo.create_segments.return_value = None
uc = CreateTemplateUseCase(repo)
cmd = CreateTemplateCommand(
user_id="u1",
name="我的模板",
mode=EditingMode.ONE_TAKE.value,
segments=[SegmentCommand(segment_order=1, duration_min=5, duration_max=10)],
)
result = uc.execute(cmd)
assert result.name == "我的模板"
assert result.mode == EditingMode.ONE_TAKE.value
assert result.user_id == "u1"
assert len(result.segments) == 1
repo.create.assert_called_once()
repo.create_segments.assert_called_once()
def test_create_invalid_mode_raises(self):
repo = MagicMock()
uc = CreateTemplateUseCase(repo)
cmd = CreateTemplateCommand(
user_id="u1",
name="test",
mode="invalid_mode",
)
with pytest.raises(ValidationError, match="无效的剪辑模式"):
uc.execute(cmd)
def test_create_with_multiple_segments(self):
repo = MagicMock()
repo.create.side_effect = lambda t: t
repo.create_segments.return_value = None
uc = CreateTemplateUseCase(repo)
cmd = CreateTemplateCommand(
user_id="u1",
name="多片段模板",
mode=EditingMode.VOICE_OVER.value,
segments=[
SegmentCommand(segment_order=1, duration_min=3, duration_max=5, material_type="人物"),
SegmentCommand(segment_order=2, duration_min=5, duration_max=8, material_type="场景"),
],
)
result = uc.execute(cmd)
assert len(result.segments) == 2
assert result.segments[0].segment_order == 1
assert result.segments[1].segment_order == 2
def test_create_with_empty_segments(self):
repo = MagicMock()
repo.create.side_effect = lambda t: t
repo.create_segments.return_value = None
uc = CreateTemplateUseCase(repo)
cmd = CreateTemplateCommand(
user_id="u1",
name="无片段模板",
mode=EditingMode.PIP.value,
)
result = uc.execute(cmd)
assert len(result.segments) == 0
repo.create_segments.assert_called_once_with([])
# ── ListTemplatesUseCase ────────────────────────────────────────────────────
class TestListTemplatesUseCase:
def test_list_no_filter(self):
templates = [_make_template("t1"), _make_template("t2")]
repo = MagicMock()
repo.list_by_user.return_value = templates
uc = ListTemplatesUseCase(repo)
result = uc.execute("u1", skip=0, limit=10)
assert len(result) == 2
repo.list_by_user.assert_called_once_with("u1", skip=0, limit=10)
def test_list_with_filter(self):
templates = [_make_template("t1")]
repo = MagicMock()
repo.list_by_user.return_value = templates
uc = ListTemplatesUseCase(repo)
f = ListTemplatesFilter(category="cat1", tag="tag1", keyword="test", mode="one_take")
result = uc.execute("u1", filter=f)
assert len(result) == 1
repo.list_by_user.assert_called_once_with(
"u1",
skip=0,
limit=50,
category="cat1",
tag="tag1",
keyword="test",
mode="one_take",
)
def test_list_pagination(self):
repo = MagicMock()
repo.list_by_user.return_value = []
uc = ListTemplatesUseCase(repo)
uc.execute("u1", skip=20, limit=10)
repo.list_by_user.assert_called_once_with("u1", skip=20, limit=10)
# ── CountTemplatesUseCase ───────────────────────────────────────────────────
class TestCountTemplatesUseCase:
def test_count_no_filter(self):
repo = MagicMock()
repo.count_by_user.return_value = 42
uc = CountTemplatesUseCase(repo)
result = uc.execute("u1")
assert result == 42
repo.count_by_user.assert_called_once_with("u1")
def test_count_with_filter(self):
repo = MagicMock()
repo.count_by_user.return_value = 5
uc = CountTemplatesUseCase(repo)
f = ListTemplatesFilter(category="cat1", tag="tag1", keyword="kw", mode="pip")
result = uc.execute("u1", filter=f)
assert result == 5
repo.count_by_user.assert_called_once_with(
"u1",
category="cat1",
tag="tag1",
keyword="kw",
mode="pip",
)
# ── GetTemplateUseCase ──────────────────────────────────────────────────────
class TestGetTemplateUseCase:
def test_get_found(self):
template = _make_template()
repo = MagicMock()
repo.get.return_value = template
uc = GetTemplateUseCase(repo)
result = uc.execute("t1", "u1")
assert result.id == "t1"
repo.get.assert_called_once_with("t1", "u1")
def test_get_not_found(self):
repo = MagicMock()
repo.get.return_value = None
uc = GetTemplateUseCase(repo)
result = uc.execute("nonexistent", "u1")
assert result is None
# ── UpdateTemplateUseCase ───────────────────────────────────────────────────
class TestUpdateTemplateUseCase:
def test_update_name(self):
existing = _make_template(name="old")
repo = MagicMock()
repo.get.return_value = existing
repo.update.return_value = existing
repo.list_segments.return_value = existing.segments
uc = UpdateTemplateUseCase(repo)
cmd = UpdateTemplateCommand(template_id="t1", user_id="u1", name="new")
result = uc.execute(cmd)
assert result.name == "new"
repo.update.assert_called_once()
def test_update_mode(self):
existing = _make_template(mode=EditingMode.ONE_TAKE.value)
repo = MagicMock()
repo.get.return_value = existing
repo.update.return_value = existing
repo.list_segments.return_value = existing.segments
uc = UpdateTemplateUseCase(repo)
cmd = UpdateTemplateCommand(template_id="t1", user_id="u1", mode=EditingMode.PIP.value)
result = uc.execute(cmd)
assert result.mode == EditingMode.PIP.value
def test_update_invalid_mode_raises(self):
existing = _make_template()
repo = MagicMock()
repo.get.return_value = existing
uc = UpdateTemplateUseCase(repo)
cmd = UpdateTemplateCommand(template_id="t1", user_id="u1", mode="bad")
with pytest.raises(ValidationError, match="无效的剪辑模式"):
uc.execute(cmd)
def test_update_not_found_raises(self):
repo = MagicMock()
repo.get.return_value = None
uc = UpdateTemplateUseCase(repo)
cmd = UpdateTemplateCommand(template_id="t999", user_id="u1", name="x")
with pytest.raises(NotFoundError):
uc.execute(cmd)
def test_update_segments(self):
existing = _make_template(segments=_make_segments(1))
repo = MagicMock()
repo.get.return_value = existing
repo.update.return_value = existing
repo.delete_segments_by_template.return_value = None
repo.create_segments.return_value = None
uc = UpdateTemplateUseCase(repo)
cmd = UpdateTemplateCommand(
template_id="t1",
user_id="u1",
segments=[
SegmentCommand(segment_order=1, duration_min=2, duration_max=5),
SegmentCommand(segment_order=2, duration_min=3, duration_max=6),
],
)
result = uc.execute(cmd)
repo.delete_segments_by_template.assert_called_once_with("t1")
repo.create_segments.assert_called_once()
assert len(result.segments) == 2
def test_update_none_fields_not_modified(self):
existing = _make_template(name="keep_name", category="keep_cat")
repo = MagicMock()
repo.get.return_value = existing
repo.update.return_value = existing
repo.list_segments.return_value = existing.segments
uc = UpdateTemplateUseCase(repo)
# 只传 name=None, category=None 表示不修改
cmd = UpdateTemplateCommand(template_id="t1", user_id="u1")
result = uc.execute(cmd)
assert result.name == "keep_name"
assert result.category == "keep_cat"
# ── DeleteTemplateUseCase ───────────────────────────────────────────────────
class TestDeleteTemplateUseCase:
def test_delete_success(self):
repo = MagicMock()
repo.delete.return_value = True
uc = DeleteTemplateUseCase(repo)
result = uc.execute("t1", "u1")
assert result is True
repo.delete.assert_called_once_with("t1", "u1")
def test_delete_not_found(self):
repo = MagicMock()
repo.delete.return_value = False
uc = DeleteTemplateUseCase(repo)
result = uc.execute("t999", "u1")
assert result is False
# ── CopyTemplateUseCase ─────────────────────────────────────────────────────
class TestCopyTemplateUseCase:
def test_copy_success(self):
copied = _make_template("t2", name="副本")
repo = MagicMock()
repo.get.return_value = _make_template("t1")
repo.copy_template.return_value = copied
uc = CopyTemplateUseCase(repo)
cmd = CopyTemplateCommand(template_id="t1", user_id="u1", new_name="副本")
result = uc.execute(cmd)
assert result.name == "副本"
repo.copy_template.assert_called_once_with("t1", "u1", "副本")
def test_copy_not_found_raises(self):
repo = MagicMock()
repo.get.return_value = None
uc = CopyTemplateUseCase(repo)
cmd = CopyTemplateCommand(template_id="t999", user_id="u1", new_name="副本")
with pytest.raises(NotFoundError):
uc.execute(cmd)
def test_copy_empty_name_raises(self):
repo = MagicMock()
repo.get.return_value = _make_template()
uc = CopyTemplateUseCase(repo)
cmd = CopyTemplateCommand(template_id="t1", user_id="u1", new_name="")
with pytest.raises(ValidationError, match="新模板名称不能为空"):
uc.execute(cmd)
def test_copy_whitespace_name_raises(self):
repo = MagicMock()
repo.get.return_value = _make_template()
uc = CopyTemplateUseCase(repo)
cmd = CopyTemplateCommand(template_id="t1", user_id="u1", new_name=" ")
with pytest.raises(ValidationError, match="新模板名称不能为空"):
uc.execute(cmd)
def test_copy_name_stripped(self):
copied = _make_template("t2", name="副本")
repo = MagicMock()
repo.get.return_value = _make_template("t1")
repo.copy_template.return_value = copied
uc = CopyTemplateUseCase(repo)
cmd = CopyTemplateCommand(template_id="t1", user_id="u1", new_name=" 副本 ")
result = uc.execute(cmd)
# 会被strip后传给repository
repo.copy_template.assert_called_once_with("t1", "u1", "副本")
# ── ValidateTemplateUseCase ─────────────────────────────────────────────────
class TestValidateTemplateUseCase:
def test_one_take_one_segment_ok(self):
template = _make_template(mode=EditingMode.ONE_TAKE.value, segments=_make_segments(1))
repo = MagicMock()
repo.get.return_value = template
uc = ValidateTemplateUseCase(repo)
cmd = ValidateTemplateCommand(template_id="t1", user_id="u1")
result = uc.execute(cmd)
assert isinstance(result, ValidateResult)
assert result.template.id == "t1"
assert len(result.warnings) == 0
def test_one_take_zero_segments_raises(self):
template = _make_template(mode=EditingMode.ONE_TAKE.value, segments=[])
repo = MagicMock()
repo.get.return_value = template
uc = ValidateTemplateUseCase(repo)
cmd = ValidateTemplateCommand(template_id="t1", user_id="u1")
with pytest.raises(ValidationError, match="一镜到底模式必须恰好有 1 个片段"):
uc.execute(cmd)
def test_one_take_multiple_segments_raises(self):
template = _make_template(mode=EditingMode.ONE_TAKE.value, segments=_make_segments(3))
repo = MagicMock()
repo.get.return_value = template
uc = ValidateTemplateUseCase(repo)
cmd = ValidateTemplateCommand(template_id="t1", user_id="u1")
with pytest.raises(ValidationError):
uc.execute(cmd)
def test_voice_over_valid_material_types(self):
segments = [
TemplateSegment(
id="s1", template_id="t1", segment_order=1, duration_min=3, duration_max=5, material_type="人物"
),
TemplateSegment(
id="s2", template_id="t1", segment_order=2, duration_min=5, duration_max=8, material_type="场景"
),
]
template = _make_template(mode=EditingMode.VOICE_OVER.value, segments=segments)
repo = MagicMock()
repo.get.return_value = template
uc = ValidateTemplateUseCase(repo)
cmd = ValidateTemplateCommand(template_id="t1", user_id="u1")
result = uc.execute(cmd)
assert len(result.warnings) == 0
def test_voice_over_missing_material_type_raises(self):
segments = [
TemplateSegment(
id="s1", template_id="t1", segment_order=1, duration_min=3, duration_max=5, material_type=None
),
]
template = _make_template(mode=EditingMode.VOICE_OVER.value, segments=segments)
repo = MagicMock()
repo.get.return_value = template
uc = ValidateTemplateUseCase(repo)
cmd = ValidateTemplateCommand(template_id="t1", user_id="u1")
with pytest.raises(ValidationError, match="material_type"):
uc.execute(cmd)
def test_voice_over_invalid_material_type_raises(self):
segments = [
TemplateSegment(
id="s1", template_id="t1", segment_order=1, duration_min=3, duration_max=5, material_type="动物"
),
]
template = _make_template(mode=EditingMode.VOICE_OVER.value, segments=segments)
repo = MagicMock()
repo.get.return_value = template
uc = ValidateTemplateUseCase(repo)
cmd = ValidateTemplateCommand(template_id="t1", user_id="u1")
with pytest.raises(ValidationError, match="material_type"):
uc.execute(cmd)
def test_voice_over_second_segment_invalid(self):
segments = [
TemplateSegment(
id="s1", template_id="t1", segment_order=1, duration_min=3, duration_max=5, material_type="人物"
),
TemplateSegment(
id="s2", template_id="t1", segment_order=2, duration_min=5, duration_max=8, material_type="bad"
),
]
template = _make_template(mode=EditingMode.VOICE_OVER.value, segments=segments)
repo = MagicMock()
repo.get.return_value = template
uc = ValidateTemplateUseCase(repo)
cmd = ValidateTemplateCommand(template_id="t1", user_id="u1")
with pytest.raises(ValidationError) as exc_info:
uc.execute(cmd)
# 报错应该提到片段2
assert "2" in str(exc_info.value)
def test_voice_duration_mismatch_warning(self):
template = _make_template(
mode=EditingMode.VOICE_OVER.value,
estimated_duration=100.0,
segments=_make_segments(2, material_type="人物"),
)
repo = MagicMock()
repo.get.return_value = template
uc = ValidateTemplateUseCase(repo)
# 配音时长只有50s,预估100s,偏差50% > 30%
cmd = ValidateTemplateCommand(template_id="t1", user_id="u1", voiceover_duration=50.0)
result = uc.execute(cmd)
assert len(result.warnings) == 1
w = result.warnings[0]
assert isinstance(w, GenerateWarning)
assert w.code == "voiceover_duration_mismatch"
assert "偏差超过" in w.message
def test_voice_duration_match_no_warning(self):
template = _make_template(
mode=EditingMode.VOICE_OVER.value,
estimated_duration=100.0,
segments=_make_segments(1, material_type="人物"),
)
repo = MagicMock()
repo.get.return_value = template
uc = ValidateTemplateUseCase(repo)
# 配音时长95s,预估100s,偏差5% < 30%
cmd = ValidateTemplateCommand(template_id="t1", user_id="u1", voiceover_duration=95.0)
result = uc.execute(cmd)
assert len(result.warnings) == 0
def test_voice_duration_at_30_percent_boundary_lower(self):
# 恰好 0.7 边界不触发
template = _make_template(mode=EditingMode.PIP.value, estimated_duration=100.0)
repo = MagicMock()
repo.get.return_value = template
uc = ValidateTemplateUseCase(repo)
cmd = ValidateTemplateCommand(template_id="t1", user_id="u1", voiceover_duration=70.0)
result = uc.execute(cmd)
# 恰好 0.7,不算 < 0.7,应该不触发
assert len(result.warnings) == 0
def test_voice_duration_below_70_percent_triggers(self):
template = _make_template(mode=EditingMode.PIP.value, estimated_duration=100.0)
repo = MagicMock()
repo.get.return_value = template
uc = ValidateTemplateUseCase(repo)
cmd = ValidateTemplateCommand(template_id="t1", user_id="u1", voiceover_duration=69.0)
result = uc.execute(cmd)
assert len(result.warnings) == 1
def test_voice_duration_above_130_percent_triggers(self):
template = _make_template(mode=EditingMode.PIP.value, estimated_duration=100.0)
repo = MagicMock()
repo.get.return_value = template
uc = ValidateTemplateUseCase(repo)
cmd = ValidateTemplateCommand(template_id="t1", user_id="u1", voiceover_duration=131.0)
result = uc.execute(cmd)
assert len(result.warnings) == 1
def test_voice_duration_zero_estimated_skip(self):
# estimated_duration = 0 不会做比例计算
template = _make_template(estimated_duration=0.0)
repo = MagicMock()
repo.get.return_value = template
uc = ValidateTemplateUseCase(repo)
cmd = ValidateTemplateCommand(template_id="t1", user_id="u1", voiceover_duration=10.0)
result = uc.execute(cmd)
assert len(result.warnings) == 0
def test_voiceover_duration_none_no_warning(self):
template = _make_template()
repo = MagicMock()
repo.get.return_value = template
uc = ValidateTemplateUseCase(repo)
cmd = ValidateTemplateCommand(template_id="t1", user_id="u1")
result = uc.execute(cmd)
assert len(result.warnings) == 0
def test_validate_not_found_raises(self):
repo = MagicMock()
repo.get.return_value = None
uc = ValidateTemplateUseCase(repo)
cmd = ValidateTemplateCommand(template_id="t999", user_id="u1")
with pytest.raises(NotFoundError):
uc.execute(cmd)
def test_warning_details_structure(self):
template = _make_template(estimated_duration=100.0)
repo = MagicMock()
repo.get.return_value = template
uc = ValidateTemplateUseCase(repo)
cmd = ValidateTemplateCommand(template_id="t1", user_id="u1", voiceover_duration=200.0)
result = uc.execute(cmd)
assert len(result.warnings) == 1
details = result.warnings[0].details
assert "voiceover_duration" in details
assert "estimated_duration" in details
assert "ratio" in details
assert details["voiceover_duration"] == 200.0
assert details["estimated_duration"] == 100.0
assert details["ratio"] == 2.0
# ── Category Use Cases ──────────────────────────────────────────────────────
class TestCategoryUseCases:
def test_create_category(self):
cat = TemplateCategory(id="c1", user_id="u1", name="分类A")
repo = MagicMock()
repo.create_category.return_value = cat
uc = CreateCategoryUseCase(repo)
cmd = CreateCategoryCommand(user_id="u1", name="分类A")
result = uc.execute(cmd)
assert result.name == "分类A"
repo.create_category.assert_called_once()
def test_list_categories(self):
cats = [TemplateCategory(id="c1", user_id="u1", name="A"), TemplateCategory(id="c2", user_id="u1", name="B")]
repo = MagicMock()
repo.list_categories.return_value = cats
uc = ListCategoriesUseCase(repo)
result = uc.execute("u1")
assert len(result) == 2
repo.list_categories.assert_called_once_with("u1")
def test_delete_category(self):
repo = MagicMock()
repo.delete_category.return_value = True
uc = DeleteCategoryUseCase(repo)
result = uc.execute("c1", "u1")
assert result is True
repo.delete_category.assert_called_once_with("c1", "u1")
# ── Tags Use Case ───────────────────────────────────────────────────────────
class TestListTagsUseCase:
def test_list_tags(self):
repo = MagicMock()
repo.list_tags.return_value = ["tag1", "tag2", "tag3"]
uc = ListTagsUseCase(repo)
result = uc.execute("u1")
assert result == ["tag1", "tag2", "tag3"]
repo.list_tags.assert_called_once_with("u1")
# ── Usage Stats Use Case ────────────────────────────────────────────────────
class TestGetTemplateUsageUseCase:
def test_get_usage(self):
repo = MagicMock()
repo.get_usage_count.return_value = 5
uc = GetTemplateUsageUseCase(repo)
result = uc.execute("t1")
assert result == 5
repo.get_usage_count.assert_called_once_with("t1")
def test_get_usage_zero(self):
repo = MagicMock()
repo.get_usage_count.return_value = 0
uc = GetTemplateUsageUseCase(repo)
result = uc.execute("t999")
assert result == 0