"""config_schemas 领域层单元测试 - 配置 schema / 枚举 / 标准化函数""" import copy import pytest from pydantic import ValidationError from packages.domain.config_schemas import ( DEFAULT_EDIT_PLAN_CONFIG, DEFAULT_EDIT_TEMPLATE_CONFIG, BGMConfig, BGMSource, CoverConfig, CoverType, EditPlanConfigSchema, EditTemplateConfigSchema, ExportConfig, FilterConfig, ShadowConfig, StrokeConfig, SubtitleConfig, TextAnimation, TextPosition, TitleConfig, normalize_plan_config, normalize_template_config, ) class TestEnums: """枚举类型测试""" def test_cover_type_values(self): assert CoverType.AI_FRAME == "ai_frame" assert CoverType.MANUAL == "manual" assert CoverType.UPLOAD == "upload" assert CoverType.AI_REGENERATE == "ai_regenerate" def test_text_position_values(self): assert TextPosition.TOP == "top" assert TextPosition.CENTER == "center" assert TextPosition.BOTTOM == "bottom" def test_text_animation_values(self): assert TextAnimation.NONE == "none" assert TextAnimation.FADE_IN == "fade_in" assert TextAnimation.SLIDE_UP == "slide_up" assert TextAnimation.SLIDE_DOWN == "slide_down" assert TextAnimation.SCALE == "scale" def test_bgm_source_values(self): assert BGMSource.LIBRARY == "library" assert BGMSource.UPLOAD == "upload" assert BGMSource.AI_RECOMMEND == "ai_recommend" def test_enums_are_str_enum(self): """枚举都是 str 类型""" assert isinstance(CoverType.AI_FRAME, str) assert isinstance(TextPosition.TOP, str) assert isinstance(TextAnimation.FADE_IN, str) assert isinstance(BGMSource.LIBRARY, str) class TestStrokeConfig: """StrokeConfig 测试""" def test_defaults(self): config = StrokeConfig() assert config.enabled is False assert config.color == "#000000" assert config.width == 1 def test_width_min(self): config = StrokeConfig(width=1) assert config.width == 1 def test_width_max(self): config = StrokeConfig(width=10) assert config.width == 10 def test_width_below_min_raises(self): with pytest.raises(ValidationError): StrokeConfig(width=0) def test_width_above_max_raises(self): with pytest.raises(ValidationError): StrokeConfig(width=11) class TestShadowConfig: """ShadowConfig 测试""" def test_defaults(self): config = ShadowConfig() assert config.enabled is False assert config.blur == 4 assert config.offset_x == 2 assert config.offset_y == 2 def test_blur_min(self): config = ShadowConfig(blur=0) assert config.blur == 0 def test_blur_max(self): config = ShadowConfig(blur=20) assert config.blur == 20 def test_blur_below_min_raises(self): with pytest.raises(ValidationError): ShadowConfig(blur=-1) def test_blur_above_max_raises(self): with pytest.raises(ValidationError): ShadowConfig(blur=21) class TestCoverConfig: """CoverConfig 测试""" def test_defaults(self): config = CoverConfig() assert config.type == CoverType.AI_FRAME assert config.image_url == "" assert config.frame_time is None def test_frame_time_ge_zero(self): config = CoverConfig(frame_time=0.0) assert config.frame_time == 0.0 def test_frame_time_negative_raises(self): with pytest.raises(ValidationError): CoverConfig(frame_time=-1.0) def test_custom_values(self): config = CoverConfig( type=CoverType.MANUAL, image_url="http://example.com/cover.jpg", frame_time=5.5, ) assert config.type == CoverType.MANUAL assert config.image_url == "http://example.com/cover.jpg" assert config.frame_time == 5.5 class TestTitleConfig: """TitleConfig 测试""" def test_defaults(self): config = TitleConfig() assert config.enabled is True assert config.ai_auto is True assert config.text == "" assert config.position == TextPosition.TOP assert config.font == "思源黑体" assert config.color == "#ffffff" assert config.size == 48 assert config.bold is True assert config.italic is False assert isinstance(config.stroke, StrokeConfig) assert isinstance(config.shadow, ShadowConfig) def test_size_min(self): config = TitleConfig(size=12) assert config.size == 12 def test_size_max(self): config = TitleConfig(size=120) assert config.size == 120 def test_size_below_min_raises(self): with pytest.raises(ValidationError): TitleConfig(size=11) def test_size_above_max_raises(self): with pytest.raises(ValidationError): TitleConfig(size=121) class TestSubtitleConfig: """SubtitleConfig 测试""" def test_defaults(self): config = SubtitleConfig() assert config.enabled is True assert config.position == TextPosition.BOTTOM assert config.font == "思源黑体" assert config.color == "#ffffff" assert config.size == 24 assert config.animation == TextAnimation.FADE_IN assert config.auto_generated is False assert config.language == "" assert config.max_chars_per_line == 20 assert config.min_chars_per_segment == 8 def test_size_min(self): config = SubtitleConfig(size=12) assert config.size == 12 def test_size_max(self): config = SubtitleConfig(size=60) assert config.size == 60 def test_size_out_of_range_raises(self): with pytest.raises(ValidationError): SubtitleConfig(size=61) def test_max_chars_per_line_range(self): config = SubtitleConfig(max_chars_per_line=40) assert config.max_chars_per_line == 40 with pytest.raises(ValidationError): SubtitleConfig(max_chars_per_line=7) with pytest.raises(ValidationError): SubtitleConfig(max_chars_per_line=41) def test_min_chars_per_segment_range(self): config = SubtitleConfig(min_chars_per_segment=20) assert config.min_chars_per_segment == 20 with pytest.raises(ValidationError): SubtitleConfig(min_chars_per_segment=1) with pytest.raises(ValidationError): SubtitleConfig(min_chars_per_segment=21) class TestBGMConfig: """BGMConfig 测试""" def test_defaults(self): config = BGMConfig() assert config.enabled is False assert config.source == BGMSource.LIBRARY assert config.asset_id == "" assert config.preset_id == "" assert config.audio_url == "" assert config.volume == 0.3 assert config.fade_in == 0.0 assert config.fade_out == 0.0 assert config.loop_enabled is True assert config.sidechain_enabled is False assert config.sidechain_ratio == 0.3 assert config.sidechain_attack == 0.02 assert config.sidechain_release == 0.5 assert config.sidechain_threshold == -25.0 def test_volume_range(self): with pytest.raises(ValidationError): BGMConfig(volume=-0.1) with pytest.raises(ValidationError): BGMConfig(volume=1.1) def test_fade_range(self): with pytest.raises(ValidationError): BGMConfig(fade_in=31.0) with pytest.raises(ValidationError): BGMConfig(fade_out=31.0) def test_sidechain_ratio_range(self): with pytest.raises(ValidationError): BGMConfig(sidechain_ratio=1.1) with pytest.raises(ValidationError): BGMConfig(sidechain_ratio=-0.1) def test_sidechain_attack_range(self): with pytest.raises(ValidationError): BGMConfig(sidechain_attack=0.0001) with pytest.raises(ValidationError): BGMConfig(sidechain_attack=1.1) def test_sidechain_release_range(self): with pytest.raises(ValidationError): BGMConfig(sidechain_release=0.001) with pytest.raises(ValidationError): BGMConfig(sidechain_release=5.1) def test_sidechain_threshold_range(self): with pytest.raises(ValidationError): BGMConfig(sidechain_threshold=-61.0) with pytest.raises(ValidationError): BGMConfig(sidechain_threshold=1.0) class TestExportConfig: """ExportConfig 测试""" def test_defaults(self): config = ExportConfig() assert config.resolution == "1080x1920" assert config.fps == 30 assert config.video_bitrate == 8000 assert config.audio_bitrate == 128 assert config.format == "mp4" assert config.quality_preset == "balanced" assert config.watermark_enabled is False assert config.watermark_text == "" def test_fps_range(self): with pytest.raises(ValidationError): ExportConfig(fps=14) with pytest.raises(ValidationError): ExportConfig(fps=61) def test_video_bitrate_range(self): with pytest.raises(ValidationError): ExportConfig(video_bitrate=999) with pytest.raises(ValidationError): ExportConfig(video_bitrate=20001) def test_audio_bitrate_range(self): with pytest.raises(ValidationError): ExportConfig(audio_bitrate=63) with pytest.raises(ValidationError): ExportConfig(audio_bitrate=321) class TestFilterConfig: """FilterConfig 测试""" def test_defaults(self): config = FilterConfig() assert config.enabled is False assert config.preset_id == "filter_none" assert config.intensity == 100 assert config.brightness == 0.0 assert config.contrast == 1.0 assert config.saturation == 1.0 assert config.warmth == 0.0 def test_intensity_range(self): with pytest.raises(ValidationError): FilterConfig(intensity=-1) with pytest.raises(ValidationError): FilterConfig(intensity=101) def test_brightness_range(self): with pytest.raises(ValidationError): FilterConfig(brightness=-1.1) with pytest.raises(ValidationError): FilterConfig(brightness=1.1) def test_contrast_range(self): with pytest.raises(ValidationError): FilterConfig(contrast=-0.1) with pytest.raises(ValidationError): FilterConfig(contrast=2.1) def test_saturation_range(self): with pytest.raises(ValidationError): FilterConfig(saturation=-0.1) with pytest.raises(ValidationError): FilterConfig(saturation=3.1) def test_warmth_range(self): with pytest.raises(ValidationError): FilterConfig(warmth=-1.1) with pytest.raises(ValidationError): FilterConfig(warmth=1.1) class TestEditPlanConfigSchema: """EditPlanConfigSchema 完整配置测试""" def test_defaults(self): config = EditPlanConfigSchema() assert isinstance(config.cover, CoverConfig) assert isinstance(config.title, TitleConfig) assert isinstance(config.subtitle, SubtitleConfig) assert isinstance(config.bgm, BGMConfig) assert isinstance(config.export, ExportConfig) assert isinstance(config.filter, FilterConfig) assert config.editing_mode == "one_take" def test_partial_update(self): """部分字段更新,其他保持默认""" config = EditPlanConfigSchema( title={"text": "自定义标题", "size": 36}, bgm={"enabled": True, "volume": 0.5}, ) assert config.title.text == "自定义标题" assert config.title.size == 36 assert config.title.font == "思源黑体" # 其他字段默认 assert config.bgm.enabled is True assert config.bgm.volume == 0.5 assert config.cover.type == CoverType.AI_FRAME # 未设置的保持默认 class TestEditTemplateConfigSchema: """EditTemplateConfigSchema 测试""" def test_defaults(self): config = EditTemplateConfigSchema() assert isinstance(config.cover, CoverConfig) assert config.editing_mode == "one_take" assert config.transition_enabled is True def test_custom_transition_enabled(self): config = EditTemplateConfigSchema(transition_enabled=False) assert config.transition_enabled is False class TestDefaultConfigs: """默认配置常量测试""" def test_plan_config_structure(self): assert "cover" in DEFAULT_EDIT_PLAN_CONFIG assert "title" in DEFAULT_EDIT_PLAN_CONFIG assert "subtitle" in DEFAULT_EDIT_PLAN_CONFIG assert "bgm" in DEFAULT_EDIT_PLAN_CONFIG assert "export" in DEFAULT_EDIT_PLAN_CONFIG assert "filter" in DEFAULT_EDIT_PLAN_CONFIG assert "editing_mode" in DEFAULT_EDIT_PLAN_CONFIG def test_template_config_has_transition_enabled(self): assert "transition_enabled" in DEFAULT_EDIT_TEMPLATE_CONFIG assert DEFAULT_EDIT_TEMPLATE_CONFIG["transition_enabled"] is True def test_template_inherits_from_plan(self): """模板配置继承计划配置的所有字段""" for key in DEFAULT_EDIT_PLAN_CONFIG: assert key in DEFAULT_EDIT_TEMPLATE_CONFIG class TestNormalizePlanConfig: """normalize_plan_config 工具函数测试""" def test_none_returns_default_copy(self): result = normalize_plan_config(None) assert result == DEFAULT_EDIT_PLAN_CONFIG # 确保是深拷贝 result["title"]["text"] = "modified" assert DEFAULT_EDIT_PLAN_CONFIG["title"]["text"] == "" def test_empty_dict_returns_default(self): result = normalize_plan_config({}) assert result == DEFAULT_EDIT_PLAN_CONFIG def test_updates_cover_section(self): result = normalize_plan_config({"cover": {"type": "manual", "frame_time": 5.0}}) assert result["cover"]["type"] == "manual" assert result["cover"]["frame_time"] == 5.0 assert result["cover"]["image_url"] == "" # 默认保留 def test_updates_title_section(self): result = normalize_plan_config({"title": {"text": "hello", "size": 32}}) assert result["title"]["text"] == "hello" assert result["title"]["size"] == 32 assert result["title"]["font"] == "思源黑体" def test_updates_bgm_section(self): result = normalize_plan_config({"bgm": {"enabled": True, "volume": 0.8}}) assert result["bgm"]["enabled"] is True assert result["bgm"]["volume"] == 0.8 def test_updates_editing_mode(self): result = normalize_plan_config({"editing_mode": "smart"}) assert result["editing_mode"] == "smart" def test_preserves_extra_fields(self): """非标准字段被保留""" result = normalize_plan_config({"custom_field": "value", "generation_task_id": "task-1"}) assert result["custom_field"] == "value" assert result["generation_task_id"] == "task-1" def test_ignores_non_dict_section(self): """section 不是 dict 时忽略""" result = normalize_plan_config({"cover": "not_a_dict"}) assert result["cover"] == DEFAULT_EDIT_PLAN_CONFIG["cover"] def test_ignores_non_str_editing_mode(self): result = normalize_plan_config({"editing_mode": 123}) assert result["editing_mode"] == "one_take" def test_deep_copy_independence(self): """修改结果不影响默认值""" result = normalize_plan_config({}) result["title"]["size"] = 999 assert DEFAULT_EDIT_PLAN_CONFIG["title"]["size"] == 48 class TestNormalizeTemplateConfig: """normalize_template_config 工具函数测试""" def test_none_returns_default_copy(self): result = normalize_template_config(None) assert result == DEFAULT_EDIT_TEMPLATE_CONFIG def test_updates_transition_enabled(self): result = normalize_template_config({"transition_enabled": False}) assert result["transition_enabled"] is False def test_ignores_non_bool_transition_enabled(self): result = normalize_template_config({"transition_enabled": "yes"}) assert result["transition_enabled"] is True def test_updates_sections(self): result = normalize_template_config( { "title": {"text": "模板标题"}, "bgm": {"enabled": True}, } ) assert result["title"]["text"] == "模板标题" assert result["bgm"]["enabled"] is True def test_preserves_extra_fields(self): result = normalize_template_config({"custom": "value"}) assert result["custom"] == "value" def test_deep_copy_independence(self): result = normalize_template_config({}) result["title"]["font"] = "CustomFont" assert DEFAULT_EDIT_TEMPLATE_CONFIG["title"]["font"] == "思源黑体"