e25fd86171
Deploy / Staging E2E Tests (push) Has been skipped
Deploy / Build Production Runtime Images (push) Has been skipped
Deploy / Deploy Production (push) Has been skipped
Deploy / Production Browser E2E (push) Has been skipped
Deploy / Deploy Staging (push) Failing after 137h58m6s
CI/CD Pipeline / Frontend Lint (push) Failing after 137h58m12s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 137h58m12s
504 lines
20 KiB
Python
504 lines
20 KiB
Python
"""Unit tests for Phase 8 任务 2.02: TemplateClipConfig + EditPlanClip 数据模型."""
|
|
|
|
import pytest
|
|
|
|
from packages.domain.edit_plan_clip import (
|
|
EditPlanClip,
|
|
EditPlanClipStatus,
|
|
)
|
|
from packages.domain.template_clip_config import (
|
|
ClipType,
|
|
TemplateClipConfig,
|
|
TransitionEffect,
|
|
)
|
|
|
|
# ── TemplateClipConfig 领域实体测试 ─────────────────────────────────────────
|
|
|
|
|
|
class TestTemplateClipConfig:
|
|
"""TemplateClipConfig 领域实体测试"""
|
|
|
|
def test_create_basic(self):
|
|
"""基本创建"""
|
|
config = TemplateClipConfig.create(
|
|
template_id="tpl_001",
|
|
clip_type=ClipType.INTRO,
|
|
order=0,
|
|
)
|
|
assert config.template_id == "tpl_001"
|
|
assert config.clip_type == ClipType.INTRO
|
|
assert config.order == 0
|
|
assert config.min_duration == 0.0
|
|
assert config.max_duration == 0.0
|
|
assert config.text_template == ""
|
|
assert config.material_requirements == {}
|
|
assert config.transition_effect == TransitionEffect.CUT
|
|
assert config.config == {}
|
|
assert config.id # 自动生成 ID
|
|
|
|
def test_create_with_all_fields(self):
|
|
"""完整字段创建"""
|
|
config = TemplateClipConfig.create(
|
|
template_id="tpl_001",
|
|
clip_type=ClipType.MAIN,
|
|
order=1,
|
|
min_duration=3.0,
|
|
max_duration=10.0,
|
|
text_template="欢迎使用{product_name}",
|
|
material_requirements={"type": "video", "min_resolution": "1080p"},
|
|
transition_effect=TransitionEffect.FADE,
|
|
config={"speed": 1.0},
|
|
)
|
|
assert config.min_duration == 3.0
|
|
assert config.max_duration == 10.0
|
|
assert config.text_template == "欢迎使用{product_name}"
|
|
assert config.material_requirements == {"type": "video", "min_resolution": "1080p"}
|
|
assert config.transition_effect == TransitionEffect.FADE
|
|
assert config.config == {"speed": 1.0}
|
|
|
|
def test_create_with_string_enum_values(self):
|
|
"""字符串枚举值创建"""
|
|
config = TemplateClipConfig.create(
|
|
template_id="tpl_001",
|
|
clip_type="outro",
|
|
order=2,
|
|
transition_effect="slide_left",
|
|
)
|
|
assert config.clip_type == ClipType.OUTRO
|
|
assert config.transition_effect == TransitionEffect.SLIDE_LEFT
|
|
|
|
def test_create_empty_template_id_raises(self):
|
|
"""空 template_id 报错"""
|
|
with pytest.raises(ValueError, match="template_id 不能为空"):
|
|
TemplateClipConfig.create(template_id="", clip_type=ClipType.INTRO, order=0)
|
|
|
|
def test_create_whitespace_template_id_raises(self):
|
|
"""空白 template_id 报错"""
|
|
with pytest.raises(ValueError, match="template_id 不能为空"):
|
|
TemplateClipConfig.create(template_id=" ", clip_type=ClipType.INTRO, order=0)
|
|
|
|
def test_create_negative_min_duration_raises(self):
|
|
"""负数 min_duration 报错"""
|
|
with pytest.raises(ValueError, match="min_duration 不能为负数"):
|
|
TemplateClipConfig.create(template_id="tpl_001", clip_type=ClipType.INTRO, order=0, min_duration=-1.0)
|
|
|
|
def test_create_negative_max_duration_raises(self):
|
|
"""负数 max_duration 报错"""
|
|
with pytest.raises(ValueError, match="max_duration 不能为负数"):
|
|
TemplateClipConfig.create(template_id="tpl_001", clip_type=ClipType.INTRO, order=0, max_duration=-1.0)
|
|
|
|
def test_create_min_greater_than_max_raises(self):
|
|
"""min_duration > max_duration 报错"""
|
|
with pytest.raises(ValueError, match="min_duration 不能大于 max_duration"):
|
|
TemplateClipConfig.create(
|
|
template_id="tpl_001",
|
|
clip_type=ClipType.INTRO,
|
|
order=0,
|
|
min_duration=10.0,
|
|
max_duration=5.0,
|
|
)
|
|
|
|
def test_has_duration_range(self):
|
|
"""has_duration_range 属性"""
|
|
config_no_range = TemplateClipConfig.create(template_id="tpl_001", clip_type=ClipType.INTRO, order=0)
|
|
assert config_no_range.has_duration_range is False
|
|
|
|
config_with_range = TemplateClipConfig.create(
|
|
template_id="tpl_001", clip_type=ClipType.INTRO, order=0, min_duration=3.0, max_duration=10.0
|
|
)
|
|
assert config_with_range.has_duration_range is True
|
|
|
|
def test_default_duration(self):
|
|
"""default_duration 属性"""
|
|
# 无时长范围
|
|
config_no_range = TemplateClipConfig.create(template_id="tpl_001", clip_type=ClipType.INTRO, order=0)
|
|
assert config_no_range.default_duration == 0.0
|
|
|
|
# 只有 min
|
|
config_min_only = TemplateClipConfig.create(
|
|
template_id="tpl_001", clip_type=ClipType.INTRO, order=0, min_duration=5.0
|
|
)
|
|
assert config_min_only.default_duration == 5.0
|
|
|
|
# 只有 max
|
|
config_max_only = TemplateClipConfig.create(
|
|
template_id="tpl_001", clip_type=ClipType.INTRO, order=0, max_duration=10.0
|
|
)
|
|
assert config_max_only.default_duration == 10.0
|
|
|
|
# 两者都有 → 平均值
|
|
config_both = TemplateClipConfig.create(
|
|
template_id="tpl_001", clip_type=ClipType.INTRO, order=0, min_duration=4.0, max_duration=10.0
|
|
)
|
|
assert config_both.default_duration == 7.0
|
|
|
|
def test_clip_type_enum_values(self):
|
|
"""ClipType 枚举值"""
|
|
assert ClipType.INTRO == "intro"
|
|
assert ClipType.MAIN == "main"
|
|
assert ClipType.TRANSITION == "transition"
|
|
assert ClipType.OUTRO == "outro"
|
|
assert ClipType.TITLE == "title"
|
|
assert ClipType.SUBTITLE == "subtitle"
|
|
|
|
def test_transition_effect_enum_values(self):
|
|
"""TransitionEffect 枚举值"""
|
|
assert TransitionEffect.CUT == "cut"
|
|
assert TransitionEffect.FADE == "fade"
|
|
assert TransitionEffect.SLIDE_LEFT == "slide_left"
|
|
assert TransitionEffect.SLIDE_RIGHT == "slide_right"
|
|
assert TransitionEffect.DISSOLVE == "dissolve"
|
|
assert TransitionEffect.WIPE == "wipe"
|
|
|
|
def test_timestamps_auto_set(self):
|
|
"""创建时自动设置时间戳"""
|
|
config = TemplateClipConfig.create(template_id="tpl_001", clip_type=ClipType.INTRO, order=0)
|
|
assert config.created_at is not None
|
|
assert config.updated_at is not None
|
|
|
|
|
|
# ── EditPlanClip 领域实体测试 ───────────────────────────────────────────────
|
|
|
|
|
|
class TestEditPlanClip:
|
|
"""EditPlanClip 领域实体测试"""
|
|
|
|
def test_create_basic(self):
|
|
"""基本创建"""
|
|
clip = EditPlanClip.create(plan_id="plan_001", clip_type="main", order=0)
|
|
assert clip.plan_id == "plan_001"
|
|
assert clip.clip_type == "main"
|
|
assert clip.order == 0
|
|
assert clip.status == EditPlanClipStatus.PENDING
|
|
assert clip.template_clip_config_id == ""
|
|
assert clip.asset_id == ""
|
|
assert clip.text_content == ""
|
|
assert clip.start_time == 0.0
|
|
assert clip.duration == 0.0
|
|
assert clip.transition_effect == "cut"
|
|
assert clip.config == {}
|
|
assert clip.id
|
|
|
|
def test_create_with_all_fields(self):
|
|
"""完整字段创建"""
|
|
clip = EditPlanClip.create(
|
|
plan_id="plan_001",
|
|
clip_type="intro",
|
|
order=0,
|
|
template_clip_config_id="cfg_001",
|
|
asset_id="asset_001",
|
|
text_content="欢迎",
|
|
start_time=0.0,
|
|
duration=5.0,
|
|
transition_effect="fade",
|
|
config={"zoom": 1.2},
|
|
)
|
|
assert clip.template_clip_config_id == "cfg_001"
|
|
assert clip.asset_id == "asset_001"
|
|
assert clip.text_content == "欢迎"
|
|
assert clip.duration == 5.0
|
|
assert clip.transition_effect == "fade"
|
|
assert clip.config == {"zoom": 1.2}
|
|
|
|
def test_create_empty_plan_id_raises(self):
|
|
"""空 plan_id 报错"""
|
|
with pytest.raises(ValueError, match="plan_id 不能为空"):
|
|
EditPlanClip.create(plan_id="", clip_type="main", order=0)
|
|
|
|
def test_create_empty_clip_type_raises(self):
|
|
"""空 clip_type 报错"""
|
|
with pytest.raises(ValueError, match="clip_type 不能为空"):
|
|
EditPlanClip.create(plan_id="plan_001", clip_type="", order=0)
|
|
|
|
def test_create_negative_start_time_raises(self):
|
|
"""负数 start_time 报错"""
|
|
with pytest.raises(ValueError, match="start_time 不能为负数"):
|
|
EditPlanClip.create(plan_id="plan_001", clip_type="main", order=0, start_time=-1.0)
|
|
|
|
def test_create_negative_duration_raises(self):
|
|
"""负数 duration 报错"""
|
|
with pytest.raises(ValueError, match="duration 不能为负数"):
|
|
EditPlanClip.create(plan_id="plan_001", clip_type="main", order=0, duration=-1.0)
|
|
|
|
def test_assign_asset(self):
|
|
"""分配素材"""
|
|
clip = EditPlanClip.create(plan_id="plan_001", clip_type="main", order=0)
|
|
assert clip.has_asset is False
|
|
clip.assign_asset("asset_001")
|
|
assert clip.asset_id == "asset_001"
|
|
assert clip.has_asset is True
|
|
|
|
def test_assign_asset_empty_raises(self):
|
|
"""分配空素材报错"""
|
|
clip = EditPlanClip.create(plan_id="plan_001", clip_type="main", order=0)
|
|
with pytest.raises(ValueError, match="asset_id 不能为空"):
|
|
clip.assign_asset("")
|
|
|
|
def test_status_transitions(self):
|
|
"""状态流转: pending → ready → rendered"""
|
|
clip = EditPlanClip.create(plan_id="plan_001", clip_type="main", order=0)
|
|
assert clip.status == EditPlanClipStatus.PENDING
|
|
|
|
clip.mark_ready()
|
|
assert clip.status == EditPlanClipStatus.READY
|
|
|
|
clip.mark_rendered()
|
|
assert clip.status == EditPlanClipStatus.RENDERED
|
|
|
|
def test_status_transition_pending_to_failed(self):
|
|
"""状态流转: pending → ready → failed"""
|
|
clip = EditPlanClip.create(plan_id="plan_001", clip_type="main", order=0)
|
|
clip.mark_ready()
|
|
clip.mark_failed()
|
|
assert clip.status == EditPlanClipStatus.FAILED
|
|
|
|
def test_mark_ready_from_non_pending_raises(self):
|
|
"""非 pending 状态标记 ready 报错"""
|
|
clip = EditPlanClip.create(plan_id="plan_001", clip_type="main", order=0)
|
|
clip.mark_ready()
|
|
with pytest.raises(ValueError, match="只有 pending 状态"):
|
|
clip.mark_ready()
|
|
|
|
def test_mark_rendered_from_non_ready_raises(self):
|
|
"""非 ready 状态标记 rendered 报错"""
|
|
clip = EditPlanClip.create(plan_id="plan_001", clip_type="main", order=0)
|
|
with pytest.raises(ValueError, match="只有 ready 状态"):
|
|
clip.mark_rendered()
|
|
|
|
def test_mark_failed_from_non_ready_raises(self):
|
|
"""非 ready 状态标记 failed 报错"""
|
|
clip = EditPlanClip.create(plan_id="plan_001", clip_type="main", order=0)
|
|
with pytest.raises(ValueError, match="只有 ready 状态"):
|
|
clip.mark_failed()
|
|
|
|
def test_end_time_property(self):
|
|
"""end_time 属性"""
|
|
clip = EditPlanClip.create(plan_id="plan_001", clip_type="main", order=0, start_time=5.0, duration=10.0)
|
|
assert clip.end_time == 15.0
|
|
|
|
def test_has_asset_property(self):
|
|
"""has_asset 属性"""
|
|
clip_no_asset = EditPlanClip.create(plan_id="plan_001", clip_type="main", order=0)
|
|
assert clip_no_asset.has_asset is False
|
|
|
|
clip_with_asset = EditPlanClip.create(plan_id="plan_001", clip_type="main", order=0, asset_id="asset_001")
|
|
assert clip_with_asset.has_asset is True
|
|
|
|
def test_edit_plan_clip_status_enum(self):
|
|
"""EditPlanClipStatus 枚举值"""
|
|
assert EditPlanClipStatus.PENDING == "pending"
|
|
assert EditPlanClipStatus.READY == "ready"
|
|
assert EditPlanClipStatus.RENDERED == "rendered"
|
|
assert EditPlanClipStatus.FAILED == "failed"
|
|
|
|
|
|
# ── Repository 集成测试(使用 SQLite 内存数据库)────────────────────────────
|
|
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import sessionmaker
|
|
|
|
from packages.adapters.sqlalchemy_impl.edit_plan_clip_repository import (
|
|
SQLAlchemyEditPlanClipRepository,
|
|
)
|
|
from packages.adapters.sqlalchemy_impl.models import Base, EditPlanClipModel, TemplateClipConfigModel
|
|
from packages.adapters.sqlalchemy_impl.template_clip_config_repository import (
|
|
SQLAlchemyTemplateClipConfigRepository,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def db_session():
|
|
"""创建内存数据库 session"""
|
|
engine = create_engine("sqlite:///:memory:")
|
|
Base.metadata.create_all(engine)
|
|
Session = sessionmaker(bind=engine)
|
|
session = Session()
|
|
yield session
|
|
session.close()
|
|
|
|
|
|
class TestTemplateClipConfigRepository:
|
|
"""TemplateClipConfig 仓储测试"""
|
|
|
|
def test_create_and_get(self, db_session):
|
|
"""创建并获取"""
|
|
repo = SQLAlchemyTemplateClipConfigRepository(db_session)
|
|
config = TemplateClipConfig.create(template_id="tpl_001", clip_type=ClipType.INTRO, order=0, min_duration=2.0)
|
|
created = repo.create(config)
|
|
assert created.id == config.id
|
|
|
|
fetched = repo.get(config.id)
|
|
assert fetched is not None
|
|
assert fetched.template_id == "tpl_001"
|
|
assert fetched.clip_type == ClipType.INTRO
|
|
assert fetched.min_duration == 2.0
|
|
|
|
def test_list_by_template(self, db_session):
|
|
"""按模板列出"""
|
|
repo = SQLAlchemyTemplateClipConfigRepository(db_session)
|
|
for i in range(3):
|
|
repo.create(TemplateClipConfig.create(template_id="tpl_001", clip_type=ClipType.MAIN, order=i))
|
|
repo.create(TemplateClipConfig.create(template_id="tpl_002", clip_type=ClipType.INTRO, order=0))
|
|
|
|
results = repo.list_by_template("tpl_001")
|
|
assert len(results) == 3
|
|
assert all(r.template_id == "tpl_001" for r in results)
|
|
# 按 order 排序
|
|
assert results[0].order == 0
|
|
assert results[1].order == 1
|
|
assert results[2].order == 2
|
|
|
|
def test_list_by_template_with_clip_type_filter(self, db_session):
|
|
"""按模板+类型过滤"""
|
|
repo = SQLAlchemyTemplateClipConfigRepository(db_session)
|
|
repo.create(TemplateClipConfig.create(template_id="tpl_001", clip_type=ClipType.INTRO, order=0))
|
|
repo.create(TemplateClipConfig.create(template_id="tpl_001", clip_type=ClipType.MAIN, order=1))
|
|
repo.create(TemplateClipConfig.create(template_id="tpl_001", clip_type=ClipType.MAIN, order=2))
|
|
|
|
results = repo.list_by_template("tpl_001", clip_type=ClipType.MAIN)
|
|
assert len(results) == 2
|
|
assert all(r.clip_type == ClipType.MAIN for r in results)
|
|
|
|
def test_update(self, db_session):
|
|
"""更新"""
|
|
repo = SQLAlchemyTemplateClipConfigRepository(db_session)
|
|
config = TemplateClipConfig.create(template_id="tpl_001", clip_type=ClipType.INTRO, order=0)
|
|
repo.create(config)
|
|
|
|
config.min_duration = 5.0
|
|
config.max_duration = 15.0
|
|
config.text_template = "Hello {name}"
|
|
updated = repo.update(config)
|
|
assert updated.min_duration == 5.0
|
|
assert updated.max_duration == 15.0
|
|
assert updated.text_template == "Hello {name}"
|
|
|
|
def test_delete(self, db_session):
|
|
"""删除"""
|
|
repo = SQLAlchemyTemplateClipConfigRepository(db_session)
|
|
config = TemplateClipConfig.create(template_id="tpl_001", clip_type=ClipType.INTRO, order=0)
|
|
repo.create(config)
|
|
assert repo.delete(config.id) is True
|
|
assert repo.get(config.id) is None
|
|
assert repo.delete("nonexistent") is False
|
|
|
|
def test_delete_by_template(self, db_session):
|
|
"""按模板批量删除"""
|
|
repo = SQLAlchemyTemplateClipConfigRepository(db_session)
|
|
for i in range(3):
|
|
repo.create(TemplateClipConfig.create(template_id="tpl_001", clip_type=ClipType.MAIN, order=i))
|
|
deleted = repo.delete_by_template("tpl_001")
|
|
assert deleted == 3
|
|
assert repo.count(template_id="tpl_001") == 0
|
|
|
|
def test_count(self, db_session):
|
|
"""统计"""
|
|
repo = SQLAlchemyTemplateClipConfigRepository(db_session)
|
|
assert repo.count() == 0
|
|
repo.create(TemplateClipConfig.create(template_id="tpl_001", clip_type=ClipType.INTRO, order=0))
|
|
repo.create(TemplateClipConfig.create(template_id="tpl_001", clip_type=ClipType.MAIN, order=1))
|
|
assert repo.count() == 2
|
|
assert repo.count(template_id="tpl_001") == 2
|
|
assert repo.count(template_id="tpl_999") == 0
|
|
|
|
|
|
class TestEditPlanClipRepository:
|
|
"""EditPlanClip 仓储测试"""
|
|
|
|
def test_create_and_get(self, db_session):
|
|
"""创建并获取"""
|
|
repo = SQLAlchemyEditPlanClipRepository(db_session)
|
|
clip = EditPlanClip.create(plan_id="plan_001", clip_type="main", order=0, duration=5.0)
|
|
created = repo.create(clip)
|
|
assert created.id == clip.id
|
|
|
|
fetched = repo.get(clip.id)
|
|
assert fetched is not None
|
|
assert fetched.plan_id == "plan_001"
|
|
assert fetched.clip_type == "main"
|
|
assert fetched.duration == 5.0
|
|
assert fetched.status == EditPlanClipStatus.PENDING
|
|
|
|
def test_list_by_plan(self, db_session):
|
|
"""按计划列出"""
|
|
repo = SQLAlchemyEditPlanClipRepository(db_session)
|
|
for i in range(3):
|
|
repo.create(EditPlanClip.create(plan_id="plan_001", clip_type="main", order=i))
|
|
repo.create(EditPlanClip.create(plan_id="plan_002", clip_type="intro", order=0))
|
|
|
|
results = repo.list_by_plan("plan_001")
|
|
assert len(results) == 3
|
|
assert all(r.plan_id == "plan_001" for r in results)
|
|
assert results[0].order == 0
|
|
assert results[1].order == 1
|
|
assert results[2].order == 2
|
|
|
|
def test_list_by_plan_with_status_filter(self, db_session):
|
|
"""按计划+状态过滤"""
|
|
repo = SQLAlchemyEditPlanClipRepository(db_session)
|
|
clip1 = EditPlanClip.create(plan_id="plan_001", clip_type="main", order=0)
|
|
clip2 = EditPlanClip.create(plan_id="plan_001", clip_type="main", order=1)
|
|
repo.create(clip1)
|
|
repo.create(clip2)
|
|
|
|
clip1.mark_ready()
|
|
repo.update(clip1)
|
|
|
|
results = repo.list_by_plan("plan_001", status=EditPlanClipStatus.READY)
|
|
assert len(results) == 1
|
|
assert results[0].status == EditPlanClipStatus.READY
|
|
|
|
def test_update(self, db_session):
|
|
"""更新"""
|
|
repo = SQLAlchemyEditPlanClipRepository(db_session)
|
|
clip = EditPlanClip.create(plan_id="plan_001", clip_type="main", order=0)
|
|
repo.create(clip)
|
|
|
|
clip.assign_asset("asset_001")
|
|
clip.duration = 10.0
|
|
updated = repo.update(clip)
|
|
assert updated.asset_id == "asset_001"
|
|
assert updated.duration == 10.0
|
|
|
|
def test_delete(self, db_session):
|
|
"""删除"""
|
|
repo = SQLAlchemyEditPlanClipRepository(db_session)
|
|
clip = EditPlanClip.create(plan_id="plan_001", clip_type="main", order=0)
|
|
repo.create(clip)
|
|
assert repo.delete(clip.id) is True
|
|
assert repo.get(clip.id) is None
|
|
assert repo.delete("nonexistent") is False
|
|
|
|
def test_delete_by_plan(self, db_session):
|
|
"""按计划批量删除"""
|
|
repo = SQLAlchemyEditPlanClipRepository(db_session)
|
|
for i in range(3):
|
|
repo.create(EditPlanClip.create(plan_id="plan_001", clip_type="main", order=i))
|
|
deleted = repo.delete_by_plan("plan_001")
|
|
assert deleted == 3
|
|
assert repo.count(plan_id="plan_001") == 0
|
|
|
|
def test_count(self, db_session):
|
|
"""统计"""
|
|
repo = SQLAlchemyEditPlanClipRepository(db_session)
|
|
assert repo.count() == 0
|
|
repo.create(EditPlanClip.create(plan_id="plan_001", clip_type="main", order=0))
|
|
repo.create(EditPlanClip.create(plan_id="plan_001", clip_type="main", order=1))
|
|
assert repo.count() == 2
|
|
assert repo.count(plan_id="plan_001") == 2
|
|
assert repo.count(plan_id="plan_999") == 0
|
|
|
|
def test_count_with_status(self, db_session):
|
|
"""按状态统计"""
|
|
repo = SQLAlchemyEditPlanClipRepository(db_session)
|
|
clip1 = EditPlanClip.create(plan_id="plan_001", clip_type="main", order=0)
|
|
clip2 = EditPlanClip.create(plan_id="plan_001", clip_type="main", order=1)
|
|
repo.create(clip1)
|
|
repo.create(clip2)
|
|
|
|
clip1.mark_ready()
|
|
repo.update(clip1)
|
|
|
|
assert repo.count(status=EditPlanClipStatus.PENDING) == 1
|
|
assert repo.count(status=EditPlanClipStatus.READY) == 1
|