Files
xiaoxia-saas/tests/unit/test_template_use_cases.py
xiaoxia 4016f0eca8
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
test(p3-1): 第29波 template use cases 单测 (39个) (#736)
2026-07-22 22:42:12 +08:00

639 lines
21 KiB
Python
Executable File

"""Template use cases 单元测试."""
from __future__ import annotations
from typing import List, Optional
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,
NotFoundError,
UpdateTemplateUseCase,
ValidateResult,
ValidateTemplateUseCase,
ValidationError,
)
from packages.domain.editing_mode import EditingMode
from packages.domain.template import Template, TemplateCategory, TemplateSegment
def _make_template(
template_id: str = "tpl_001",
user_id: str = "user_001",
name: str = "测试模板",
mode: str = "one_take",
segments: Optional[List[TemplateSegment]] = None,
estimated_duration: float = 60.0,
) -> Template:
tpl = Template(
id=template_id,
user_id=user_id,
name=name,
mode=mode,
category="测试分类",
tags=["tag1", "tag2"],
title_config={"enabled": True},
subtitle_config={"enabled": False},
bgm_config={"enabled": True},
estimated_duration=estimated_duration,
)
if segments is not None:
tpl.segments = segments
return tpl
def _make_segments(count: int = 1, material_type: Optional[str] = None) -> List[TemplateSegment]:
return [
TemplateSegment(
id=f"seg_{i}",
template_id="tpl_001",
segment_order=i,
duration_min=3.0,
duration_max=8.0,
material_type=material_type,
)
for i in range(count)
]
class TestCreateTemplateUseCase:
def test_creates_template_with_segments(self) -> None:
repo = MagicMock()
repo.create.side_effect = lambda t: t # 返回传入的template
repo.create_segments.return_value = None
use_case = CreateTemplateUseCase(repo)
cmd = CreateTemplateCommand(
user_id="user_001",
name="新模板",
mode=EditingMode.ONE_TAKE.value,
category="分类A",
tags=["t1", "t2"],
segments=[
SegmentCommand(segment_order=0, duration_min=2.0, duration_max=5.0),
SegmentCommand(segment_order=1, duration_min=3.0, duration_max=6.0),
],
)
result = use_case.execute(cmd)
assert result.name == "新模板"
assert result.mode == EditingMode.ONE_TAKE.value
assert len(result.segments) == 2
assert result.segments[0].segment_order == 0
assert result.segments[1].segment_order == 1
repo.create.assert_called_once()
repo.create_segments.assert_called_once()
def test_invalid_mode_raises_validation_error(self) -> None:
repo = MagicMock()
use_case = CreateTemplateUseCase(repo)
cmd = CreateTemplateCommand(
user_id="user_001",
name="测试",
mode="invalid_mode",
segments=[],
)
with pytest.raises(ValidationError, match="无效的剪辑模式"):
use_case.execute(cmd)
def test_creates_without_segments(self) -> None:
repo = MagicMock()
repo.create.side_effect = lambda t: t
repo.create_segments.return_value = None
use_case = CreateTemplateUseCase(repo)
cmd = CreateTemplateCommand(
user_id="user_001",
name="空片段模板",
mode=EditingMode.ONE_TAKE.value,
segments=[],
)
result = use_case.execute(cmd)
assert len(result.segments) == 0
repo.create_segments.assert_called_once_with([])
def test_generates_uuid_for_template_and_segments(self) -> None:
repo = MagicMock()
repo.create.side_effect = lambda t: t
repo.create_segments.return_value = None
use_case = CreateTemplateUseCase(repo)
cmd = CreateTemplateCommand(
user_id="user_001",
name="UUID测试",
mode=EditingMode.VOICE_OVER.value,
segments=[
SegmentCommand(segment_order=0, duration_min=1.0, duration_max=3.0, material_type="人物"),
],
)
result = use_case.execute(cmd)
assert len(result.id) == 32 # uuid hex
assert len(result.segments[0].id) == 32
assert result.segments[0].template_id == result.id
class TestListTemplatesUseCase:
def test_list_without_filter(self) -> None:
repo = MagicMock()
expected = [_make_template("t1"), _make_template("t2")]
repo.list_by_user.return_value = expected
use_case = ListTemplatesUseCase(repo)
result = use_case.execute("user_001", skip=0, limit=10)
assert len(result) == 2
repo.list_by_user.assert_called_once_with("user_001", skip=0, limit=10)
def test_list_with_filter(self) -> None:
repo = MagicMock()
expected = [_make_template("t1")]
repo.list_by_user.return_value = expected
use_case = ListTemplatesUseCase(repo)
f = ListTemplatesFilter(category="分类A", tag="t1", keyword="测试", mode="one_take")
result = use_case.execute("user_001", skip=0, limit=10, filter=f)
assert len(result) == 1
repo.list_by_user.assert_called_once_with(
"user_001",
skip=0,
limit=10,
category="分类A",
tag="t1",
keyword="测试",
mode="one_take",
)
class TestCountTemplatesUseCase:
def test_count_without_filter(self) -> None:
repo = MagicMock()
repo.count_by_user.return_value = 42
use_case = CountTemplatesUseCase(repo)
result = use_case.execute("user_001")
assert result == 42
repo.count_by_user.assert_called_once_with("user_001")
def test_count_with_filter(self) -> None:
repo = MagicMock()
repo.count_by_user.return_value = 5
use_case = CountTemplatesUseCase(repo)
f = ListTemplatesFilter(category="分类A")
result = use_case.execute("user_001", filter=f)
assert result == 5
repo.count_by_user.assert_called_once_with(
"user_001",
category="分类A",
tag=None,
keyword=None,
mode=None,
)
class TestGetTemplateUseCase:
def test_returns_template_when_found(self) -> None:
repo = MagicMock()
expected = _make_template()
repo.get.return_value = expected
use_case = GetTemplateUseCase(repo)
result = use_case.execute("tpl_001", "user_001")
assert result is expected
repo.get.assert_called_once_with("tpl_001", "user_001")
def test_returns_none_when_not_found(self) -> None:
repo = MagicMock()
repo.get.return_value = None
use_case = GetTemplateUseCase(repo)
result = use_case.execute("nonexistent", "user_001")
assert result is None
class TestUpdateTemplateUseCase:
def test_updates_name_and_tags(self) -> None:
repo = MagicMock()
existing = _make_template()
existing.segments = _make_segments(2)
repo.get.return_value = existing
repo.update.side_effect = lambda t: t
repo.list_segments.return_value = existing.segments
use_case = UpdateTemplateUseCase(repo)
cmd = UpdateTemplateCommand(
template_id="tpl_001",
user_id="user_001",
name="新名字",
tags=["new_tag"],
)
result = use_case.execute(cmd)
assert result.name == "新名字"
assert result.tags == ["new_tag"]
# mode没变
assert result.mode == EditingMode.ONE_TAKE.value
repo.update.assert_called_once()
def test_not_found_raises(self) -> None:
repo = MagicMock()
repo.get.return_value = None
use_case = UpdateTemplateUseCase(repo)
cmd = UpdateTemplateCommand(template_id="nonexistent", user_id="user_001", name="x")
with pytest.raises(NotFoundError):
use_case.execute(cmd)
def test_invalid_mode_raises(self) -> None:
repo = MagicMock()
repo.get.return_value = _make_template()
use_case = UpdateTemplateUseCase(repo)
cmd = UpdateTemplateCommand(
template_id="tpl_001",
user_id="user_001",
mode="invalid",
)
with pytest.raises(ValidationError, match="无效的剪辑模式"):
use_case.execute(cmd)
def test_replaces_segments_when_provided(self) -> None:
repo = MagicMock()
existing = _make_template()
existing.segments = _make_segments(2)
repo.get.return_value = existing
repo.update.side_effect = lambda t: t
repo.delete_segments_by_template.return_value = None
repo.create_segments.return_value = None
use_case = UpdateTemplateUseCase(repo)
cmd = UpdateTemplateCommand(
template_id="tpl_001",
user_id="user_001",
segments=[
SegmentCommand(segment_order=0, duration_min=1.0, duration_max=2.0),
SegmentCommand(segment_order=1, duration_min=3.0, duration_max=4.0),
SegmentCommand(segment_order=2, duration_min=5.0, duration_max=6.0),
],
)
result = use_case.execute(cmd)
assert len(result.segments) == 3
repo.delete_segments_by_template.assert_called_once_with("tpl_001")
repo.create_segments.assert_called_once()
def test_no_segments_keeps_existing(self) -> None:
repo = MagicMock()
existing = _make_template()
existing.segments = _make_segments(3)
repo.get.return_value = existing
repo.update.side_effect = lambda t: t
repo.list_segments.return_value = existing.segments
use_case = UpdateTemplateUseCase(repo)
cmd = UpdateTemplateCommand(
template_id="tpl_001",
user_id="user_001",
name="只改名字",
)
result = use_case.execute(cmd)
assert len(result.segments) == 3
repo.delete_segments_by_template.assert_not_called()
repo.create_segments.assert_not_called()
repo.list_segments.assert_called_once_with("tpl_001")
class TestDeleteTemplateUseCase:
def test_delete_success(self) -> None:
repo = MagicMock()
repo.delete.return_value = True
use_case = DeleteTemplateUseCase(repo)
result = use_case.execute("tpl_001", "user_001")
assert result is True
repo.delete.assert_called_once_with("tpl_001", "user_001")
def test_delete_not_found(self) -> None:
repo = MagicMock()
repo.delete.return_value = False
use_case = DeleteTemplateUseCase(repo)
result = use_case.execute("nonexistent", "user_001")
assert result is False
class TestCopyTemplateUseCase:
def test_copy_success(self) -> None:
repo = MagicMock()
original = _make_template(name="原模板")
repo.get.return_value = original
copied = _make_template(template_id="copied_001", name="原模板 副本")
repo.copy_template.return_value = copied
use_case = CopyTemplateUseCase(repo)
cmd = CopyTemplateCommand(
template_id="tpl_001",
user_id="user_001",
new_name="原模板 副本",
)
result = use_case.execute(cmd)
assert result.name == "原模板 副本"
repo.copy_template.assert_called_once_with("tpl_001", "user_001", "原模板 副本")
def test_not_found_raises(self) -> None:
repo = MagicMock()
repo.get.return_value = None
use_case = CopyTemplateUseCase(repo)
cmd = CopyTemplateCommand(template_id="no", user_id="u1", new_name="x")
with pytest.raises(NotFoundError):
use_case.execute(cmd)
def test_empty_name_raises(self) -> None:
repo = MagicMock()
repo.get.return_value = _make_template()
use_case = CopyTemplateUseCase(repo)
cmd = CopyTemplateCommand(template_id="t1", user_id="u1", new_name=" ")
with pytest.raises(ValidationError, match="名称不能为空"):
use_case.execute(cmd)
def test_name_stripped(self) -> None:
repo = MagicMock()
repo.get.return_value = _make_template()
repo.copy_template.return_value = _make_template(name="新名字")
use_case = CopyTemplateUseCase(repo)
cmd = CopyTemplateCommand(template_id="t1", user_id="u1", new_name=" 新名字 ")
use_case.execute(cmd)
repo.copy_template.assert_called_once_with("t1", "u1", "新名字")
class TestValidateTemplateUseCase:
def test_one_take_with_one_segment_passes(self) -> None:
repo = MagicMock()
tpl = _make_template(mode=EditingMode.ONE_TAKE.value)
tpl.segments = _make_segments(1)
repo.get.return_value = tpl
use_case = ValidateTemplateUseCase(repo)
cmd = ValidateTemplateCommand(template_id="tpl_001", user_id="user_001")
result = use_case.execute(cmd)
assert isinstance(result, ValidateResult)
assert result.template is tpl
assert len(result.warnings) == 0
def test_one_take_with_multiple_segments_raises(self) -> None:
repo = MagicMock()
tpl = _make_template(mode=EditingMode.ONE_TAKE.value)
tpl.segments = _make_segments(3)
repo.get.return_value = tpl
use_case = ValidateTemplateUseCase(repo)
cmd = ValidateTemplateCommand(template_id="tpl_001", user_id="user_001")
with pytest.raises(ValidationError, match="恰好有 1 个片段"):
use_case.execute(cmd)
def test_voice_over_with_valid_material_types_passes(self) -> None:
repo = MagicMock()
tpl = _make_template(mode=EditingMode.VOICE_OVER.value, estimated_duration=30.0)
tpl.segments = [
TemplateSegment(
id="s1", template_id="t1", segment_order=0, duration_min=3, duration_max=5, material_type="人物"
),
TemplateSegment(
id="s2", template_id="t1", segment_order=1, duration_min=3, duration_max=5, material_type="场景"
),
]
repo.get.return_value = tpl
use_case = ValidateTemplateUseCase(repo)
cmd = ValidateTemplateCommand(template_id="tpl_001", user_id="user_001")
result = use_case.execute(cmd)
assert len(result.warnings) == 0
def test_voice_over_missing_material_type_raises(self) -> None:
repo = MagicMock()
tpl = _make_template(mode=EditingMode.VOICE_OVER.value)
tpl.segments = [
TemplateSegment(
id="s1", template_id="t1", segment_order=0, duration_min=3, duration_max=5, material_type=None
),
]
repo.get.return_value = tpl
use_case = ValidateTemplateUseCase(repo)
cmd = ValidateTemplateCommand(template_id="tpl_001", user_id="user_001")
with pytest.raises(ValidationError, match="material_type"):
use_case.execute(cmd)
def test_voice_over_invalid_material_type_raises(self) -> None:
repo = MagicMock()
tpl = _make_template(mode=EditingMode.VOICE_OVER.value)
tpl.segments = [
TemplateSegment(
id="s1", template_id="t1", segment_order=0, duration_min=3, duration_max=5, material_type="动物"
),
]
repo.get.return_value = tpl
use_case = ValidateTemplateUseCase(repo)
cmd = ValidateTemplateCommand(template_id="tpl_001", user_id="user_001")
with pytest.raises(ValidationError, match="material_type"):
use_case.execute(cmd)
def test_voiceover_duration_within_range_no_warning(self) -> None:
repo = MagicMock()
tpl = _make_template(mode=EditingMode.ONE_TAKE.value, estimated_duration=60.0)
tpl.segments = _make_segments(1)
repo.get.return_value = tpl
use_case = ValidateTemplateUseCase(repo)
# 65s vs 60s = 1.08 ratio,在±30%内
cmd = ValidateTemplateCommand(template_id="t1", user_id="u1", voiceover_duration=65.0)
result = use_case.execute(cmd)
assert len(result.warnings) == 0
def test_voiceover_duration_too_short_warns(self) -> None:
repo = MagicMock()
tpl = _make_template(mode=EditingMode.ONE_TAKE.value, estimated_duration=60.0)
tpl.segments = _make_segments(1)
repo.get.return_value = tpl
use_case = ValidateTemplateUseCase(repo)
# 20s vs 60s = 0.33 ratio,超过±30%
cmd = ValidateTemplateCommand(template_id="t1", user_id="u1", voiceover_duration=20.0)
result = use_case.execute(cmd)
assert len(result.warnings) == 1
assert result.warnings[0].code == "voiceover_duration_mismatch"
assert "偏差超过" in result.warnings[0].message
assert result.warnings[0].details["ratio"] < 0.7
def test_voiceover_duration_too_long_warns(self) -> None:
repo = MagicMock()
tpl = _make_template(mode=EditingMode.ONE_TAKE.value, estimated_duration=60.0)
tpl.segments = _make_segments(1)
repo.get.return_value = tpl
use_case = ValidateTemplateUseCase(repo)
# 100s vs 60s = 1.67 ratio,超过±30%
cmd = ValidateTemplateCommand(template_id="t1", user_id="u1", voiceover_duration=100.0)
result = use_case.execute(cmd)
assert len(result.warnings) == 1
assert result.warnings[0].code == "voiceover_duration_mismatch"
assert result.warnings[0].details["ratio"] > 1.3
def test_zero_estimated_duration_no_warning(self) -> None:
repo = MagicMock()
tpl = _make_template(mode=EditingMode.ONE_TAKE.value, estimated_duration=0.0)
tpl.segments = _make_segments(1)
repo.get.return_value = tpl
use_case = ValidateTemplateUseCase(repo)
cmd = ValidateTemplateCommand(template_id="t1", user_id="u1", voiceover_duration=50.0)
result = use_case.execute(cmd)
# estimated_duration=0不做偏差检查
assert len(result.warnings) == 0
def test_no_voiceover_duration_no_warning(self) -> None:
repo = MagicMock()
tpl = _make_template(estimated_duration=60.0)
tpl.segments = _make_segments(1)
repo.get.return_value = tpl
use_case = ValidateTemplateUseCase(repo)
cmd = ValidateTemplateCommand(template_id="t1", user_id="u1") # 不传voiceover_duration
result = use_case.execute(cmd)
assert len(result.warnings) == 0
def test_not_found_raises(self) -> None:
repo = MagicMock()
repo.get.return_value = None
use_case = ValidateTemplateUseCase(repo)
cmd = ValidateTemplateCommand(template_id="no", user_id="u1")
with pytest.raises(NotFoundError):
use_case.execute(cmd)
class TestCategoryUseCases:
def test_create_category(self) -> None:
repo = MagicMock()
cat = TemplateCategory(id="cat_001", user_id="u1", name="新分类")
repo.create_category.return_value = cat
use_case = CreateCategoryUseCase(repo)
cmd = CreateCategoryCommand(user_id="u1", name="新分类")
result = use_case.execute(cmd)
assert result.name == "新分类"
repo.create_category.assert_called_once()
def test_list_categories(self) -> None:
repo = MagicMock()
expected = [TemplateCategory(id="c1", user_id="u1", name="A")]
repo.list_categories.return_value = expected
use_case = ListCategoriesUseCase(repo)
result = use_case.execute("u1")
assert result == expected
repo.list_categories.assert_called_once_with("u1")
def test_delete_category(self) -> None:
repo = MagicMock()
repo.delete_category.return_value = True
use_case = DeleteCategoryUseCase(repo)
result = use_case.execute("cat_001", "u1")
assert result is True
repo.delete_category.assert_called_once_with("cat_001", "u1")
class TestListTagsUseCase:
def test_returns_tags_list(self) -> None:
repo = MagicMock()
repo.list_tags.return_value = ["tag1", "tag2", "tag3"]
use_case = ListTagsUseCase(repo)
result = use_case.execute("u1")
assert result == ["tag1", "tag2", "tag3"]
repo.list_tags.assert_called_once_with("u1")
class TestGetTemplateUsageUseCase:
def test_returns_usage_count(self) -> None:
repo = MagicMock()
repo.get_usage_count.return_value = 15
use_case = GetTemplateUsageUseCase(repo)
result = use_case.execute("tpl_001")
assert result == 15
repo.get_usage_count.assert_called_once_with("tpl_001")
class TestGenerateWarning:
def test_warning_default_details(self) -> None:
w = GenerateWarning(code="test_code", message="test message")
assert w.code == "test_code"
assert w.message == "test message"
assert w.details == {}
def test_warning_with_details(self) -> None:
w = GenerateWarning(code="test", message="msg", details={"key": "value"})
assert w.details == {"key": "value"}