diff --git a/tests/unit/domain/test_edit_template.py b/tests/unit/domain/test_edit_template.py new file mode 100755 index 000000000..5d4609ba5 --- /dev/null +++ b/tests/unit/domain/test_edit_template.py @@ -0,0 +1,281 @@ +"""edit_template 剪辑模板实体单测.""" + +from datetime import datetime, timezone + +import pytest +from domain.edit_template import EditTemplate, EditTemplateStatus +from domain.editing_mode import EditingMode + +# ── EditTemplateStatus 枚举 ────────────────────────────────────────────────── + + +class TestEditTemplateStatus: + """EditTemplateStatus 枚举""" + + def test_enum_values(self): + assert EditTemplateStatus.ACTIVE.value == "active" + assert EditTemplateStatus.INACTIVE.value == "inactive" + + def test_is_str_enum(self): + assert isinstance(EditTemplateStatus.ACTIVE, str) + assert EditTemplateStatus.ACTIVE == "active" + + def test_from_string(self): + assert EditTemplateStatus("active") == EditTemplateStatus.ACTIVE + assert EditTemplateStatus("inactive") == EditTemplateStatus.INACTIVE + + def test_invalid_raises(self): + with pytest.raises(ValueError): + EditTemplateStatus("deleted") + + +# ── EditTemplate.create 工厂方法 ──────────────────────────────────────────── + + +class TestEditTemplateCreate: + """EditTemplate.create 工厂方法""" + + def test_minimal_create(self): + t = EditTemplate.create("测试模板") + assert t.id is not None + assert len(t.id) == 32 # uuid4 hex + assert t.name == "测试模板" + assert t.description == "" + assert t.template_type == "default" + assert t.editing_mode == "one_take" + assert t.config == {} + assert t.preview_url == "" + assert t.sort_weight == 0 + assert t.status == EditTemplateStatus.ACTIVE + assert t.version == 1 + + def test_unique_ids(self): + t1 = EditTemplate.create("模板A") + t2 = EditTemplate.create("模板B") + assert t1.id != t2.id + + def test_custom_fields(self): + t = EditTemplate.create( + "自定义模板", + description="这是一个自定义模板", + template_type="story", + editing_mode="one_take", + config={"key": "value"}, + preview_url="https://example.com/preview.mp4", + sort_weight=100, + status=EditTemplateStatus.INACTIVE, + version=2, + ) + assert t.name == "自定义模板" + assert t.description == "这是一个自定义模板" + assert t.template_type == "story" + assert t.editing_mode == "one_take" + assert t.config == {"key": "value"} + assert t.preview_url == "https://example.com/preview.mp4" + assert t.sort_weight == 100 + assert t.status == EditTemplateStatus.INACTIVE + assert t.version == 2 + + def test_name_stripped(self): + t = EditTemplate.create(" 带空格的模板 ") + assert t.name == "带空格的模板" + + def test_empty_name_raises(self): + with pytest.raises(ValueError, match="名称"): + EditTemplate.create("") + + def test_whitespace_only_name_raises(self): + with pytest.raises(ValueError): + EditTemplate.create(" ") + + def test_invalid_editing_mode_raises(self): + with pytest.raises(ValueError, match="editing_mode"): + EditTemplate.create("测试", editing_mode="invalid_mode") + + def test_empty_editing_mode_falls_back_to_default(self): + t = EditTemplate.create("测试", editing_mode="") + assert t.editing_mode == "one_take" + + def test_whitespace_editing_mode_falls_back(self): + t = EditTemplate.create("测试", editing_mode=" ") + assert t.editing_mode == "one_take" + + def test_editing_mode_stripped(self): + t = EditTemplate.create("测试", editing_mode=" one_take ") + assert t.editing_mode == "one_take" + + def test_description_stripped(self): + t = EditTemplate.create("测试", description=" 描述 ") + assert t.description == "描述" + + def test_template_type_stripped(self): + t = EditTemplate.create("测试", template_type=" vlog ") + assert t.template_type == "vlog" + + def test_empty_template_type_falls_back(self): + t = EditTemplate.create("测试", template_type="") + assert t.template_type == "default" + + def test_none_config_becomes_empty_dict(self): + t = EditTemplate.create("测试", config=None) + assert t.config == {} + assert isinstance(t.config, dict) + + def test_preview_url_stripped(self): + t = EditTemplate.create("测试", preview_url=" https://x.com/a.mp4 ") + assert t.preview_url == "https://x.com/a.mp4" + + def test_timestamps_are_utc(self): + t = EditTemplate.create("测试") + assert t.created_at.tzinfo is not None + assert t.updated_at.tzinfo is not None + + def test_created_at_equals_updated_at_on_create(self): + t = EditTemplate.create("测试") + # 创建时两个时间应该非常接近 + diff = abs((t.updated_at - t.created_at).total_seconds()) + assert diff < 1.0 + + +# ── 状态操作 ───────────────────────────────────────────────────────────────── + + +class TestEditTemplateStatusOperations: + """EditTemplate 状态操作""" + + def test_activate_sets_active(self): + t = EditTemplate.create("测试", status=EditTemplateStatus.INACTIVE) + t.activate() + assert t.status == EditTemplateStatus.ACTIVE + assert t.is_active is True + + def test_deactivate_sets_inactive(self): + t = EditTemplate.create("测试") + t.deactivate() + assert t.status == EditTemplateStatus.INACTIVE + assert t.is_active is False + + def test_is_active_true(self): + t = EditTemplate.create("测试") + assert t.is_active is True + + def test_is_active_false(self): + t = EditTemplate.create("测试", status=EditTemplateStatus.INACTIVE) + assert t.is_active is False + + def test_activate_updates_updated_at(self): + t = EditTemplate.create("测试", status=EditTemplateStatus.INACTIVE) + old_updated = t.updated_at + t.activate() + assert t.updated_at >= old_updated + + def test_deactivate_updates_updated_at(self): + t = EditTemplate.create("测试") + old_updated = t.updated_at + t.deactivate() + assert t.updated_at >= old_updated + + +# ── 版本操作 ───────────────────────────────────────────────────────────────── + + +class TestEditTemplateVersion: + """EditTemplate 版本操作""" + + def test_bump_version_increments(self): + t = EditTemplate.create("测试") + assert t.version == 1 + t.bump_version() + assert t.version == 2 + + def test_bump_version_multiple(self): + t = EditTemplate.create("测试", version=5) + t.bump_version() + t.bump_version() + t.bump_version() + assert t.version == 8 + + def test_bump_version_updates_updated_at(self): + t = EditTemplate.create("测试") + old_updated = t.updated_at + t.bump_version() + assert t.updated_at >= old_updated + + +# ── dataclass 基础特性 ─────────────────────────────────────────────────────── + + +class TestEditTemplateBasics: + """EditTemplate 基础特性""" + + def test_slots_no_extra_attrs(self): + t = EditTemplate.create("测试") + with pytest.raises(AttributeError): + t.nonexistent_field = "value" + + def test_direct_construction_minimal(self): + # 最小构造:仅必填字段 + 状态,其余走默认值 + t = EditTemplate( + id="custom_id", + name="直接构造", + status=EditTemplateStatus.ACTIVE, + ) + assert t.id == "custom_id" + assert t.name == "直接构造" + assert t.status == EditTemplateStatus.ACTIVE + # 默认值检查 + assert t.description == "" + assert t.config == {} + assert t.version == 1 + assert t.editing_mode == EditingMode.ONE_TAKE.value + assert isinstance(t.created_at, datetime) + assert isinstance(t.updated_at, datetime) + + def test_direct_construction_full(self): + # 完整构造:所有字段都传 + now = datetime(2025, 1, 1, tzinfo=timezone.utc) + t = EditTemplate( + id="full_id", + name="完整构造", + description="测试描述", + template_type="custom", + editing_mode=EditingMode.PIP.value, + config={"key": "value"}, + preview_url="https://example.com/preview.jpg", + sort_weight=100, + status=EditTemplateStatus.INACTIVE, + version=3, + created_at=now, + updated_at=now, + ) + assert t.id == "full_id" + assert t.name == "完整构造" + assert t.description == "测试描述" + assert t.template_type == "custom" + assert t.editing_mode == EditingMode.PIP.value + assert t.config == {"key": "value"} + assert t.preview_url == "https://example.com/preview.jpg" + assert t.sort_weight == 100 + assert t.status == EditTemplateStatus.INACTIVE + assert t.version == 3 + assert t.created_at == now + assert t.updated_at == now + + def test_config_is_independent(self): + # 不同实例的 config 应该是独立的 dict + t1 = EditTemplate.create("模板1") + t2 = EditTemplate.create("模板2") + t1.config["key"] = "value" + assert "key" not in t2.config + + def test_equality(self): + # 两个不同实例即使内容相同也不等(id不同) + t1 = EditTemplate.create("同名模板") + t2 = EditTemplate.create("同名模板") + assert t1 != t2 + + def test_same_id_equal(self): + now = datetime.now(timezone.utc) + t1 = EditTemplate(id="same", name="同名", created_at=now, updated_at=now) + t2 = EditTemplate(id="same", name="同名", created_at=now, updated_at=now) + assert t1 == t2