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
712 lines
25 KiB
Python
Executable File
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
|