Files
xiaoxia-saas/tests/unit/test_config_schemas.py
CI Bot 2df7bc9dc8
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m41s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m45s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m54s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 3m1s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m34s
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 5m49s
CI/CD Pipeline / Frontend Lint (push) Successful in 6m0s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m27s
CI/CD Pipeline / Integration Tests (push) Successful in 2m20s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m11s
CI/CD Pipeline / Unit Tests (push) Failing after 5m52s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 30s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m48s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 5m10s
style: auto-format with black + isort + prettier
2026-07-24 13:35:05 +00:00

511 lines
17 KiB
Python
Executable File

"""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"] == "思源黑体"