Files
xiaoxia-saas/tests/unit/test_domain_remaining_small_modules.py
xiaoxia 2972f19954
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 / Frontend Lint (push) Successful in 58s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m38s
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 / Validate - Migration (alembic) (push) Successful in 1m46s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m42s
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m18s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m23s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m27s
CI/CD Pipeline / Unit Tests (push) Failing after 5m54s
CI/CD Pipeline / Integration Tests (push) Successful in 2m26s
CI/CD Pipeline / Build Staging API Image (push) Successful in 11m39s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m14s
CI/CD Pipeline / ACR Image Cleanup (push) Failing after 14s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 2m3s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m14s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
test(ci): 修复10个ruff错误 + Unit Tests dedup收集失败 (#839 #881)
2026-07-25 17:38:17 +08:00

598 lines
21 KiB
Python
Executable File

from dataclasses import FrozenInstanceError
"""domain 层剩余小模块单测 - bgm_utils/exceptions/editing_mode/recipe/template/template_version/template_clip_config/preset_bgm/preset_voices/title_library/voice_library"""
import pytest
from packages.domain.bgm_utils import merge_bgm_config
from packages.domain.editing_mode import EditingMode
from packages.domain.exceptions import (
DomainError,
NotFoundError,
QuotaExceededError,
ValidationError,
)
from packages.domain.preset_bgm import (
PRESET_BGM_LIBRARY,
PresetBGM,
get_preset_bgm,
list_preset_bgm_by_style,
search_preset_bgm,
)
from packages.domain.preset_voices import (
PRESET_VOICES,
PresetVoice,
get_preset_voice_by_id,
get_preset_voices,
is_preset_voice,
)
from packages.domain.recipe import Recipe, RecipeItem
from packages.domain.template import Template, TemplateCategory, TemplateSegment
from packages.domain.template_clip_config import (
ClipType,
TemplateClipConfig,
TransitionEffect,
)
from packages.domain.template_version import EditTemplateVersion
from packages.domain.title_library import TitleLibraryItem
from packages.domain.voice_library import VoiceLibraryItem
# ── EditingMode ───────────────────────────────────────────────────────────────
class TestEditingMode:
"""EditingMode 枚举测试"""
def test_all_modes_exist(self):
assert EditingMode.ONE_TAKE == "one_take"
assert EditingMode.PIP == "pip"
assert EditingMode.VOICE_OVER == "voice_over"
assert EditingMode.VOICE_PIP == "voice_pip"
def test_str_enum(self):
assert isinstance(EditingMode.ONE_TAKE, str)
def test_four_modes(self):
assert len(EditingMode) == 4
# ── Exceptions ────────────────────────────────────────────────────────────────
class TestDomainExceptions:
"""异常类测试"""
def test_domain_error_base(self):
err = DomainError("test")
assert isinstance(err, Exception)
assert str(err) == "test"
def test_not_found_error(self):
err = NotFoundError("not found")
assert isinstance(err, DomainError)
assert str(err) == "not found"
def test_validation_error(self):
err = ValidationError("invalid")
assert isinstance(err, DomainError)
assert str(err) == "invalid"
def test_quota_exceeded_error(self):
err = QuotaExceededError(dimension="storage", limit=100, used=150)
assert isinstance(err, DomainError)
assert err.dimension == "storage"
assert err.limit == 100
assert err.used == 150
assert "storage" in str(err)
assert "150/100" in str(err)
def test_not_found_is_domain_error(self):
assert issubclass(NotFoundError, DomainError)
def test_validation_is_domain_error(self):
assert issubclass(ValidationError, DomainError)
def test_quota_exceeded_is_domain_error(self):
assert issubclass(QuotaExceededError, DomainError)
# ── BGM Utils ────────────────────────────────────────────────────────────────
class TestMergeBgmConfig:
"""merge_bgm_config 函数测试"""
def test_user_empty_returns_template_copy(self):
template = {"enabled": True, "volume": 0.5, "source": "library"}
result = merge_bgm_config(template, {})
assert result == template
# 确保是拷贝不是引用
result["volume"] = 0.9
assert template["volume"] == 0.5
def test_template_empty_returns_user_copy(self):
user = {"enabled": False, "volume": 0.3}
result = merge_bgm_config({}, user)
assert result == user
def test_user_none_returns_template(self):
template = {"enabled": True, "volume": 0.5}
result = merge_bgm_config(template, None) # type: ignore
assert result == template
def test_template_none_returns_user(self):
user = {"enabled": True, "volume": 0.5}
result = merge_bgm_config(None, user) # type: ignore
assert result == user
def test_user_overrides_template(self):
template = {"volume": 0.3, "source": "library", "asset_id": "tpl-1"}
user = {"volume": 0.8, "asset_id": "user-1"}
result = merge_bgm_config(template, user)
assert result["volume"] == 0.8
assert result["asset_id"] == "user-1"
assert result["source"] == "library" # 模板保留
def test_enabled_special_handling_user_not_set(self):
"""用户没传 enabled 时保留模板的 enabled"""
template = {"enabled": True, "volume": 0.5}
user = {"volume": 0.8}
result = merge_bgm_config(template, user)
assert result["enabled"] is True # 保留模板值
def test_enabled_user_explicit_false(self):
"""用户显式传了 enabled=False 则覆盖"""
template = {"enabled": True, "volume": 0.5}
user = {"enabled": False}
result = merge_bgm_config(template, user)
assert result["enabled"] is False
def test_enabled_user_explicit_true(self):
"""用户显式传了 enabled=True 则覆盖"""
template = {"enabled": False, "volume": 0.5}
user = {"enabled": True}
result = merge_bgm_config(template, user)
assert result["enabled"] is True
def test_full_merge(self):
"""完整合并场景"""
template = {
"enabled": True,
"source": "library",
"volume": 0.3,
"fade_in": 0.0,
"fade_out": 0.0,
"loop_enabled": True,
}
user = {
"volume": 0.7,
"asset_id": "my-bgm",
"fade_in": 1.0,
}
result = merge_bgm_config(template, user)
assert result["enabled"] is True # 保留模板
assert result["volume"] == 0.7 # 用户覆盖
assert result["source"] == "library" # 模板保留
assert result["asset_id"] == "my-bgm" # 用户新增
assert result["fade_in"] == 1.0 # 用户覆盖
assert result["fade_out"] == 0.0 # 模板保留
assert result["loop_enabled"] is True # 模板保留
# ── Recipe ───────────────────────────────────────────────────────────────────
class TestRecipe:
"""Recipe / RecipeItem 测试"""
def test_recipe_item_create(self):
item = RecipeItem(id="item-1", recipe_id="r1", item_type="asset", item_id="a1")
assert item.id == "item-1"
assert item.recipe_id == "r1"
assert item.item_type == "asset"
assert item.item_id == "a1"
assert item.position == 0
assert item.metadata_ == {}
def test_recipe_create(self):
recipe = Recipe(id="r1", user_id="u1", name="我的配方")
assert recipe.id == "r1"
assert recipe.user_id == "u1"
assert recipe.name == "我的配方"
assert recipe.description == ""
assert recipe.items == []
assert recipe.is_active is True
assert recipe.generation_params == {}
assert recipe.created_at is not None
def test_recipe_with_items(self):
items = [
RecipeItem(id="i1", recipe_id="r1", item_type="asset", item_id="a1", position=0),
RecipeItem(id="i2", recipe_id="r1", item_type="title", item_id="t1", position=1),
]
recipe = Recipe(id="r1", user_id="u1", name="test", items=items)
assert len(recipe.items) == 2
assert recipe.items[0].item_type == "asset"
assert recipe.items[1].position == 1
def test_recipe_items_independent_list(self):
"""不同 recipe 的 items 是独立列表"""
r1 = Recipe(id="r1", user_id="u1", name="r1")
r2 = Recipe(id="r2", user_id="u1", name="r2")
assert r1.items is not r2.items
# ── Template ─────────────────────────────────────────────────────────────────
class TestTemplate:
"""Template 相关实体测试"""
def test_template_segment(self):
seg = TemplateSegment(
id="seg-1",
template_id="t1",
segment_order=0,
duration_min=3.0,
duration_max=5.0,
)
assert seg.id == "seg-1"
assert seg.template_id == "t1"
assert seg.segment_order == 0
assert seg.duration_min == 3.0
assert seg.duration_max == 5.0
assert seg.material_type is None
assert seg.created_at is not None
def test_template_create(self):
tpl = Template(
id="t1",
user_id="u1",
name="口播模板",
mode="voice_over",
)
assert tpl.id == "t1"
assert tpl.user_id == "u1"
assert tpl.name == "口播模板"
assert tpl.mode == "voice_over"
assert tpl.category == ""
assert tpl.tags == []
assert tpl.segments == []
assert tpl.is_active is True
assert tpl.estimated_duration == 0.0
def test_template_with_segments(self):
segs = [
TemplateSegment(id="s1", template_id="t1", segment_order=0, duration_min=2, duration_max=4),
TemplateSegment(id="s2", template_id="t1", segment_order=1, duration_min=5, duration_max=8),
]
tpl = Template(id="t1", user_id="u1", name="test", mode="one_take", segments=segs)
assert len(tpl.segments) == 2
assert tpl.segments[0].segment_order == 0
def test_template_category(self):
cat = TemplateCategory(id="cat-1", user_id="u1", name="热门")
assert cat.id == "cat-1"
assert cat.user_id == "u1"
assert cat.name == "热门"
assert cat.created_at is not None
# ── TemplateVersion ──────────────────────────────────────────────────────────
class TestEditTemplateVersion:
"""EditTemplateVersion 测试"""
def test_create_basic(self):
v = EditTemplateVersion.create(template_id="t1", version=1)
assert v.id
assert len(v.id) == 32
assert v.template_id == "t1"
assert v.version == 1
assert v.name == ""
assert v.editing_mode == "one_take"
assert v.config == {}
assert v.clip_configs == []
assert v.change_note == ""
assert v.published_by == ""
def test_create_with_details(self):
config = {"title": {"size": 36}}
clips = [{"clip_type": "intro"}, {"clip_type": "outro"}]
v = EditTemplateVersion.create(
template_id="t1",
version=2,
name="V2 优化版",
editing_mode="voice_over",
config=config,
clip_configs=clips,
change_note="优化了节奏",
published_by="user-1",
)
assert v.version == 2
assert v.name == "V2 优化版"
assert v.editing_mode == "voice_over"
assert v.config == config
assert v.clip_configs == clips
assert v.change_note == "优化了节奏"
assert v.published_by == "user-1"
def test_create_none_config_defaults_empty(self):
v = EditTemplateVersion.create(template_id="t1", version=1, config=None)
assert v.config == {}
def test_create_none_clip_configs_defaults_empty(self):
v = EditTemplateVersion.create(template_id="t1", version=1, clip_configs=None)
assert v.clip_configs == []
# ── TemplateClipConfig ───────────────────────────────────────────────────────
class TestTemplateClipConfig:
"""TemplateClipConfig + 枚举测试"""
def test_clip_type_values(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_clip_type_count(self):
assert len(ClipType) == 6
def test_transition_effect_values(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_transition_effect_count(self):
assert len(TransitionEffect) == 6
def test_template_clip_config(self):
clip = TemplateClipConfig(
id="clip-1",
template_id="t1",
clip_type=ClipType.MAIN,
order=1,
min_duration=2.0,
max_duration=5.0,
)
assert clip.id == "clip-1"
assert clip.template_id == "t1"
assert clip.clip_type == ClipType.MAIN
assert clip.order == 1
assert clip.min_duration == 2.0
assert clip.max_duration == 5.0
def test_str_enum(self):
assert isinstance(ClipType.INTRO, str)
assert isinstance(TransitionEffect.FADE, str)
# ── PresetBGM ────────────────────────────────────────────────────────────────
class TestPresetBGM:
"""PresetBGM + 查询函数测试"""
def test_preset_bgm_create(self):
bgm = PresetBGM(
id="bgm_test_001",
name="测试音乐",
style="upbeat",
duration=120.0,
)
assert bgm.id == "bgm_test_001"
assert bgm.name == "测试音乐"
assert bgm.style == "upbeat"
assert bgm.duration == 120.0
assert bgm.artist == ""
assert bgm.tags == []
assert bgm.audio_url == ""
def test_preset_bgm_frozen(self):
bgm = PresetBGM(id="t1", name="t", style="x", duration=10.0)
with pytest.raises(FrozenInstanceError):
bgm.name = "改名"
def test_library_not_empty(self):
assert len(PRESET_BGM_LIBRARY) > 0
def test_all_presets_have_required_fields(self):
for bgm in PRESET_BGM_LIBRARY:
assert bgm.id
assert bgm.name
assert bgm.style
assert bgm.duration > 0
def test_get_preset_bgm_existing(self):
first = PRESET_BGM_LIBRARY[0]
result = get_preset_bgm(first.id)
assert result is not None
assert result.id == first.id
def test_get_preset_bgm_nonexistent(self):
assert get_preset_bgm("nonexistent_bgm") is None
def test_list_preset_bgm_by_style(self):
upbeat = list_preset_bgm_by_style("upbeat")
assert len(upbeat) > 0
assert all(b.style == "upbeat" for b in upbeat)
def test_list_preset_bgm_by_style_empty(self):
result = list_preset_bgm_by_style("nonexistent_style")
assert result == []
def test_search_preset_bgm_by_name(self):
result = search_preset_bgm("阳光")
assert len(result) >= 1
assert any("阳光" in b.name for b in result)
def test_search_preset_bgm_by_tag(self):
result = search_preset_bgm("轻快")
assert len(result) >= 1
assert any(any("轻快" in t for t in b.tags) for b in result)
def test_search_preset_bgm_empty_result(self):
result = search_preset_bgm("xyz_not_exist_keyword")
assert result == []
# ── PresetVoices ─────────────────────────────────────────────────────────────
class TestPresetVoices:
"""PresetVoice + 查询函数测试"""
def test_preset_voice_create(self):
v = PresetVoice(
voice_id="test_voice",
name="测试音色",
description="测试用",
gender="female",
)
assert v.voice_id == "test_voice"
assert v.name == "测试音色"
assert v.gender == "female"
assert v.language == "zh-CN"
assert v.preview_url == ""
assert v.tags is None
def test_preset_voice_to_dict(self):
v = PresetVoice(
voice_id="v1",
name="音色1",
description="desc",
gender="male",
language="zh-CN",
tags=["温柔", "男声"],
)
d = v.to_dict()
assert d["voice_id"] == "v1"
assert d["name"] == "音色1"
assert d["description"] == "desc"
assert d["gender"] == "male"
assert d["language"] == "zh-CN"
assert d["tags"] == ["温柔", "男声"]
def test_preset_voice_to_dict_no_tags(self):
v = PresetVoice(voice_id="v1", name="t", description="d", gender="female")
d = v.to_dict()
assert d["tags"] == []
def test_preset_voice_frozen(self):
v = PresetVoice(voice_id="v1", name="t", description="d", gender="female")
with pytest.raises(FrozenInstanceError):
v.name = "改名"
def test_preset_voices_list_not_empty(self):
assert len(PRESET_VOICES) > 0
def test_get_preset_voices(self):
voices = get_preset_voices()
assert len(voices) == len(PRESET_VOICES)
assert all(isinstance(v, PresetVoice) for v in voices)
def test_get_preset_voice_by_id_existing(self):
first = PRESET_VOICES[0]
result = get_preset_voice_by_id(first.voice_id)
assert result is not None
assert result.voice_id == first.voice_id
def test_get_preset_voice_by_id_nonexistent(self):
assert get_preset_voice_by_id("nonexistent_voice") is None
def test_is_preset_voice_true(self):
first = PRESET_VOICES[0]
assert is_preset_voice(first.voice_id) is True
def test_is_preset_voice_false(self):
assert is_preset_voice("fake_voice_id") is False
# ── TitleLibrary ─────────────────────────────────────────────────────────────
class TestTitleLibraryItem:
"""TitleLibraryItem 测试"""
def test_create_basic(self):
item = TitleLibraryItem(id="t1", user_id="u1", name="标题1", text="这是标题文案")
assert item.id == "t1"
assert item.user_id == "u1"
assert item.name == "标题1"
assert item.text == "这是标题文案"
assert item.category == "default"
assert item.tags == []
assert item.usage_count == 0
assert item.is_active is True
assert item.created_at is not None
def test_create_with_details(self):
item = TitleLibraryItem(
id="t1",
user_id="u1",
name="爆款标题",
text="三个方法教你...",
category="爆款",
description="高点击率",
tags=["热门", "干货"],
usage_count=100,
)
assert item.category == "爆款"
assert item.description == "高点击率"
assert item.tags == ["热门", "干货"]
assert item.usage_count == 100
# ── VoiceLibrary ─────────────────────────────────────────────────────────────
class TestVoiceLibraryItem:
"""VoiceLibraryItem 测试"""
def test_create_basic(self):
item = VoiceLibraryItem(id="v1", user_id="u1", name="我的配音")
assert item.id == "v1"
assert item.user_id == "u1"
assert item.name == "我的配音"
assert item.text == ""
assert item.voice_provider == ""
assert item.duration == 0
assert item.status == "completed"
assert item.project_id is None
assert item.tags == []
assert item.created_at is not None
def test_create_with_details(self):
item = VoiceLibraryItem(
id="v1",
user_id="u1",
name="产品介绍",
text="大家好,今天给大家介绍...",
voice_provider="cosyvoice",
voice_id="voice_001",
voice_name="温柔女声",
audio_url="http://cdn/audio.mp3",
duration=30.5,
file_size=102400,
status="processing",
project_id="proj-1",
tags=["产品", "介绍"],
)
assert item.voice_provider == "cosyvoice"
assert item.voice_id == "voice_001"
assert item.audio_url == "http://cdn/audio.mp3"
assert item.duration == 30.5
assert item.file_size == 102400
assert item.status == "processing"
assert item.project_id == "proj-1"
assert item.tags == ["产品", "介绍"]