"""小领域模块组合单测 — template_clip_config / tag / exceptions / editing_mode.""" from __future__ import annotations import pytest from packages.domain.editing_mode import EditingMode from packages.domain.exceptions import ( DomainError, NotFoundError, QuotaExceededError, ValidationError, ) from packages.domain.tag import Tag from packages.domain.template_clip_config import ( ClipType, TemplateClipConfig, TransitionEffect, ) # ── EditingMode ──────────────────────────────────────────────────────────── class TestEditingMode: def test_one_take(self): assert EditingMode.ONE_TAKE == "one_take" def test_pip(self): assert EditingMode.PIP == "pip" def test_voice_over(self): assert EditingMode.VOICE_OVER == "voice_over" def test_voice_pip(self): assert EditingMode.VOICE_PIP == "voice_pip" def test_from_string(self): assert EditingMode("one_take") == EditingMode.ONE_TAKE assert EditingMode("pip") == EditingMode.PIP def test_invalid_raises(self): with pytest.raises(ValueError): EditingMode("invalid_mode") def test_is_str(self): # StrEnum 实例本身就是 str assert isinstance(EditingMode.ONE_TAKE, str) assert EditingMode.ONE_TAKE + "_suffix" == "one_take_suffix" # ── 异常类 ───────────────────────────────────────────────────────────────── class TestExceptions: def test_domain_error_is_exception(self): err = DomainError("test") assert isinstance(err, Exception) assert str(err) == "test" def test_not_found_inherits_domain(self): err = NotFoundError("not found") assert isinstance(err, DomainError) assert str(err) == "not found" def test_validation_error_inherits_domain(self): err = ValidationError("bad input") assert isinstance(err, DomainError) assert str(err) == "bad input" def test_quota_exceeded_error_attributes(self): err = QuotaExceededError(dimension="storage_gb", limit=100, used=150) assert err.dimension == "storage_gb" assert err.limit == 100 assert err.used == 150 assert "storage_gb" in str(err) assert "150" in str(err) assert "100" in str(err) def test_quota_exceeded_is_domain_error(self): err = QuotaExceededError("x", 10, 20) assert isinstance(err, DomainError) def test_catch_domain_error_catches_all(self): """所有领域异常都能被 DomainError catch.""" for cls in [NotFoundError, ValidationError, QuotaExceededError]: try: if cls == QuotaExceededError: raise cls("dim", 10, 20) raise cls("msg") except DomainError: pass else: pytest.fail(f"{cls.__name__} 未被 DomainError 捕获") # ── Tag ──────────────────────────────────────────────────────────────────── class TestTag: def test_create_basic(self): tag = Tag.create(user_id="user1", name="旅行") assert tag.id assert tag.user_id == "user1" assert tag.name == "旅行" def test_create_name_stripped(self): tag = Tag.create(user_id="u1", name=" 美食 ") assert tag.name == "美食" def test_create_empty_name_raises(self): with pytest.raises(ValueError, match="标签名称不能为空"): Tag.create(user_id="u1", name="") def test_create_whitespace_name_raises(self): with pytest.raises(ValueError, match="标签名称不能为空"): Tag.create(user_id="u1", name=" ") def test_create_has_created_at(self): tag = Tag.create(user_id="u1", name="t1") assert tag.created_at is not None def test_unique_ids(self): t1 = Tag.create(user_id="u1", name="t1") t2 = Tag.create(user_id="u1", name="t2") assert t1.id != t2.id # ── ClipType 枚举 ───────────────────────────────────────────────────────── class TestClipType: def test_all_types(self): 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_from_string(self): assert ClipType("main") == ClipType.MAIN assert ClipType("intro") == ClipType.INTRO def test_invalid_raises(self): with pytest.raises(ValueError): ClipType("invalid") # ── TransitionEffect 枚举 ───────────────────────────────────────────────── class TestTransitionEffect: def test_all_effects(self): 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_from_string(self): assert TransitionEffect("fade") == TransitionEffect.FADE assert TransitionEffect("cut") == TransitionEffect.CUT def test_invalid_raises(self): with pytest.raises(ValueError): TransitionEffect("invalid_effect") # ── TemplateClipConfig.create ────────────────────────────────────────────── class TestTemplateClipConfigCreate: def test_basic_creation(self): clip = TemplateClipConfig.create( template_id="tmpl_1", clip_type=ClipType.MAIN, order=1, ) assert clip.id assert clip.template_id == "tmpl_1" assert clip.clip_type == ClipType.MAIN assert clip.order == 1 assert clip.min_duration == 0.0 assert clip.max_duration == 0.0 assert clip.text_template == "" assert clip.material_requirements == {} assert clip.transition_effect == TransitionEffect.CUT assert clip.config == {} def test_clip_type_string(self): clip = TemplateClipConfig.create( template_id="t1", clip_type="intro", order=0, ) assert clip.clip_type == ClipType.INTRO def test_template_id_stripped(self): clip = TemplateClipConfig.create( template_id=" tmpl_1 ", clip_type=ClipType.MAIN, order=1, ) assert clip.template_id == "tmpl_1" def test_empty_template_id_raises(self): with pytest.raises(ValueError, match="template_id 不能为空"): TemplateClipConfig.create(template_id="", clip_type=ClipType.MAIN, order=1) def test_whitespace_template_id_raises(self): with pytest.raises(ValueError, match="template_id 不能为空"): TemplateClipConfig.create(template_id=" ", clip_type=ClipType.MAIN, order=1) def test_negative_min_duration_raises(self): with pytest.raises(ValueError, match="min_duration 不能为负数"): TemplateClipConfig.create( template_id="t1", clip_type=ClipType.MAIN, order=1, min_duration=-1.0, ) def test_negative_max_duration_raises(self): with pytest.raises(ValueError, match="max_duration 不能为负数"): TemplateClipConfig.create( template_id="t1", clip_type=ClipType.MAIN, order=1, max_duration=-1.0, ) def test_min_greater_than_max_raises(self): with pytest.raises(ValueError, match="min_duration 不能大于 max_duration"): TemplateClipConfig.create( template_id="t1", clip_type=ClipType.MAIN, order=1, min_duration=10.0, max_duration=5.0, ) def test_zero_min_and_max_ok(self): clip = TemplateClipConfig.create( template_id="t1", clip_type=ClipType.MAIN, order=1, min_duration=0, max_duration=0, ) assert clip.min_duration == 0 assert clip.max_duration == 0 def test_min_zero_max_positive_ok(self): clip = TemplateClipConfig.create( template_id="t1", clip_type=ClipType.MAIN, order=1, min_duration=0, max_duration=10.0, ) assert clip.max_duration == 10.0 def test_min_equals_max_ok(self): clip = TemplateClipConfig.create( template_id="t1", clip_type=ClipType.MAIN, order=1, min_duration=5.0, max_duration=5.0, ) assert clip.min_duration == 5.0 assert clip.max_duration == 5.0 def test_with_text_template(self): clip = TemplateClipConfig.create( template_id="t1", clip_type=ClipType.MAIN, order=1, text_template=" 欢迎收看 {channel} ", ) # text_template 会 strip assert clip.text_template == "欢迎收看 {channel}" def test_with_material_requirements(self): reqs = {"material_type": "video", "min_duration": 3} clip = TemplateClipConfig.create( template_id="t1", clip_type=ClipType.MAIN, order=1, material_requirements=reqs, ) assert clip.material_requirements == reqs def test_material_requirements_none_defaults_empty(self): clip = TemplateClipConfig.create( template_id="t1", clip_type=ClipType.MAIN, order=1, material_requirements=None, ) assert clip.material_requirements == {} def test_transition_effect_string(self): clip = TemplateClipConfig.create( template_id="t1", clip_type=ClipType.MAIN, order=1, transition_effect="fade", ) assert clip.transition_effect == TransitionEffect.FADE def test_with_config(self): config = {"speed": 1.5, "filter": "vibrance"} clip = TemplateClipConfig.create( template_id="t1", clip_type=ClipType.MAIN, order=1, config=config, ) assert clip.config == config def test_config_none_defaults_empty(self): clip = TemplateClipConfig.create( template_id="t1", clip_type=ClipType.MAIN, order=1, config=None, ) assert clip.config == {} def test_created_at_and_updated_at(self): clip = TemplateClipConfig.create( template_id="t1", clip_type=ClipType.MAIN, order=1, ) assert clip.created_at is not None assert clip.updated_at is not None def test_unique_ids(self): c1 = TemplateClipConfig.create(template_id="t1", clip_type=ClipType.MAIN, order=1) c2 = TemplateClipConfig.create(template_id="t1", clip_type=ClipType.MAIN, order=2) assert c1.id != c2.id # ── TemplateClipConfig 属性 ──────────────────────────────────────────────── class TestTemplateClipConfigProperties: def test_has_duration_range_false_both_zero(self): clip = TemplateClipConfig.create( template_id="t1", clip_type=ClipType.MAIN, order=1, ) assert clip.has_duration_range is False def test_has_duration_range_true_min_only(self): clip = TemplateClipConfig.create( template_id="t1", clip_type=ClipType.MAIN, order=1, min_duration=2.0, ) assert clip.has_duration_range is True def test_has_duration_range_true_max_only(self): clip = TemplateClipConfig.create( template_id="t1", clip_type=ClipType.MAIN, order=1, max_duration=10.0, ) assert clip.has_duration_range is True def test_has_duration_range_true_both_set(self): clip = TemplateClipConfig.create( template_id="t1", clip_type=ClipType.MAIN, order=1, min_duration=2.0, max_duration=10.0, ) assert clip.has_duration_range is True def test_default_duration_zero(self): clip = TemplateClipConfig.create( template_id="t1", clip_type=ClipType.MAIN, order=1, ) assert clip.default_duration == 0.0 def test_default_duration_min_only(self): clip = TemplateClipConfig.create( template_id="t1", clip_type=ClipType.MAIN, order=1, min_duration=3.0, ) assert clip.default_duration == 3.0 def test_default_duration_max_only(self): clip = TemplateClipConfig.create( template_id="t1", clip_type=ClipType.MAIN, order=1, max_duration=10.0, ) assert clip.default_duration == 10.0 def test_default_duration_both_midpoint(self): clip = TemplateClipConfig.create( template_id="t1", clip_type=ClipType.MAIN, order=1, min_duration=2.0, max_duration=8.0, ) assert clip.default_duration == 5.0 def test_default_duration_min_equals_max(self): clip = TemplateClipConfig.create( template_id="t1", clip_type=ClipType.MAIN, order=1, min_duration=5.0, max_duration=5.0, ) assert clip.default_duration == 5.0