Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ad765cf150 | |||
| ba3a916a75 |
@@ -1,4 +1,6 @@
|
||||
"""领域层通用异常单元测试."""
|
||||
"""
|
||||
领域层异常类单元测试
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -14,118 +16,99 @@ class TestDomainError:
|
||||
"""DomainError 基类测试"""
|
||||
|
||||
def test_is_exception(self):
|
||||
"""DomainError 是 Exception 的子类"""
|
||||
assert issubclass(DomainError, Exception)
|
||||
|
||||
def test_can_raise_and_catch(self):
|
||||
"""可以抛出和捕获"""
|
||||
def test_raise_and_catch(self):
|
||||
with pytest.raises(DomainError):
|
||||
raise DomainError("something went wrong")
|
||||
raise DomainError("test error")
|
||||
|
||||
def test_message(self):
|
||||
"""异常消息正确"""
|
||||
err = DomainError("test message")
|
||||
assert str(err) == "test message"
|
||||
def test_error_message(self):
|
||||
err = DomainError("something went wrong")
|
||||
assert str(err) == "something went wrong"
|
||||
|
||||
def test_empty_message(self):
|
||||
err = DomainError("")
|
||||
assert str(err) == ""
|
||||
|
||||
|
||||
class TestNotFoundError:
|
||||
"""NotFoundError 测试"""
|
||||
|
||||
def test_is_domain_error(self):
|
||||
"""NotFoundError 继承自 DomainError"""
|
||||
assert issubclass(NotFoundError, DomainError)
|
||||
|
||||
def test_can_raise_as_domain_error(self):
|
||||
"""可以作为 DomainError 捕获"""
|
||||
def test_raise_and_catch_as_domain(self):
|
||||
with pytest.raises(DomainError):
|
||||
raise NotFoundError("resource not found")
|
||||
|
||||
def test_default_message(self):
|
||||
"""无参构造"""
|
||||
err = NotFoundError()
|
||||
assert isinstance(err, NotFoundError)
|
||||
def test_raise_and_catch_specific(self):
|
||||
with pytest.raises(NotFoundError):
|
||||
raise NotFoundError("not found")
|
||||
|
||||
def test_custom_message(self):
|
||||
"""自定义消息"""
|
||||
def test_error_message(self):
|
||||
err = NotFoundError("user 123 not found")
|
||||
assert str(err) == "user 123 not found"
|
||||
assert "user 123 not found" in str(err)
|
||||
|
||||
|
||||
class TestValidationError:
|
||||
"""ValidationError 测试"""
|
||||
|
||||
def test_is_domain_error(self):
|
||||
"""ValidationError 继承自 DomainError"""
|
||||
assert issubclass(ValidationError, DomainError)
|
||||
|
||||
def test_can_raise_as_domain_error(self):
|
||||
"""可以作为 DomainError 捕获"""
|
||||
def test_raise_and_catch_as_domain(self):
|
||||
with pytest.raises(DomainError):
|
||||
raise ValidationError("invalid input")
|
||||
|
||||
def test_custom_message(self):
|
||||
"""自定义消息"""
|
||||
err = ValidationError("duration must be positive")
|
||||
assert str(err) == "duration must be positive"
|
||||
def test_raise_and_catch_specific(self):
|
||||
with pytest.raises(ValidationError):
|
||||
raise ValidationError("validation failed")
|
||||
|
||||
def test_error_message(self):
|
||||
err = ValidationError("name cannot be empty")
|
||||
assert "name cannot be empty" in str(err)
|
||||
|
||||
|
||||
class TestQuotaExceededError:
|
||||
"""QuotaExceededError 测试"""
|
||||
|
||||
def test_is_domain_error(self):
|
||||
"""QuotaExceededError 继承自 DomainError"""
|
||||
assert issubclass(QuotaExceededError, DomainError)
|
||||
|
||||
def test_can_raise_as_domain_error(self):
|
||||
"""可以作为 DomainError 捕获"""
|
||||
with pytest.raises(DomainError):
|
||||
raise QuotaExceededError("storage", 1024.0, 2048.0)
|
||||
|
||||
def test_stores_dimension_limit_used(self):
|
||||
"""保存 dimension、limit、used 属性"""
|
||||
err = QuotaExceededError("storage_mb", 1024.0, 1500.0)
|
||||
assert err.dimension == "storage_mb"
|
||||
def test_constructor_sets_attributes(self):
|
||||
err = QuotaExceededError(dimension="storage", limit=1024.0, used=2048.0)
|
||||
assert err.dimension == "storage"
|
||||
assert err.limit == 1024.0
|
||||
assert err.used == 1500.0
|
||||
assert err.used == 2048.0
|
||||
|
||||
def test_error_message_format(self):
|
||||
"""异常消息格式正确"""
|
||||
err = QuotaExceededError("storage_mb", 1024.0, 1500.0)
|
||||
err = QuotaExceededError(dimension="storage", limit=1024.0, used=2048.0)
|
||||
msg = str(err)
|
||||
assert "storage_mb" in msg
|
||||
assert "1500.0" in msg
|
||||
assert "storage" in msg
|
||||
assert "2048.0" in msg
|
||||
assert "1024.0" in msg
|
||||
assert "Quota exceeded" in msg
|
||||
|
||||
def test_integer_values(self):
|
||||
"""整数值也能正常工作"""
|
||||
err = QuotaExceededError("projects", 10, 15)
|
||||
assert err.dimension == "projects"
|
||||
assert err.limit == 10
|
||||
assert err.used == 15
|
||||
def test_raise_and_catch_as_domain(self):
|
||||
with pytest.raises(DomainError):
|
||||
raise QuotaExceededError("projects", 10, 15)
|
||||
|
||||
def test_raise_and_catch_specific(self):
|
||||
with pytest.raises(QuotaExceededError):
|
||||
raise QuotaExceededError("render", 5, 10)
|
||||
|
||||
def test_zero_limit(self):
|
||||
"""限制为 0 时也能正常工作"""
|
||||
err = QuotaExceededError("custom_templates", 0, 1)
|
||||
assert err.limit == 0
|
||||
assert err.used == 1
|
||||
err = QuotaExceededError(dimension="test", limit=0.0, used=1.0)
|
||||
assert err.limit == 0.0
|
||||
assert err.used == 1.0
|
||||
|
||||
def test_negative_values(self):
|
||||
"""负数也能存(领域层不做额外校验)"""
|
||||
err = QuotaExceededError(dimension="test", limit=-5.0, used=-3.0)
|
||||
assert err.limit == -5.0
|
||||
assert err.used == -3.0
|
||||
|
||||
class TestExceptionHierarchy:
|
||||
"""异常继承关系测试"""
|
||||
|
||||
def test_all_are_domain_errors(self):
|
||||
"""所有异常都可以作为 DomainError 捕获"""
|
||||
errors = [
|
||||
NotFoundError(),
|
||||
ValidationError("bad"),
|
||||
QuotaExceededError("x", 10.0, 20.0),
|
||||
]
|
||||
for err in errors:
|
||||
assert isinstance(err, DomainError)
|
||||
|
||||
def test_distinct_types(self):
|
||||
"""不同异常类型可以区分"""
|
||||
assert not issubclass(NotFoundError, ValidationError)
|
||||
assert not issubclass(ValidationError, QuotaExceededError)
|
||||
assert not issubclass(NotFoundError, QuotaExceededError)
|
||||
def test_large_values(self):
|
||||
err = QuotaExceededError(dimension="storage", limit=1e9, used=1.5e9)
|
||||
assert err.limit == 1e9
|
||||
assert err.used == 1.5e9
|
||||
|
||||
@@ -152,3 +152,103 @@ class TestEditTemplateBumpVersion:
|
||||
old_updated = template.updated_at
|
||||
template.bump_version()
|
||||
assert template.updated_at > old_updated or template.updated_at == old_updated
|
||||
|
||||
|
||||
class TestEditTemplateExtended:
|
||||
"""EditTemplate 深度补充测试"""
|
||||
|
||||
def test_id_is_hex(self):
|
||||
t = EditTemplate.create(name="test")
|
||||
int(t.id, 16)
|
||||
|
||||
def test_ids_are_unique(self):
|
||||
t1 = EditTemplate.create(name="test1")
|
||||
t2 = EditTemplate.create(name="test2")
|
||||
assert t1.id != t2.id
|
||||
|
||||
def test_empty_description(self):
|
||||
t = EditTemplate.create(name="test", description="")
|
||||
assert t.description == ""
|
||||
|
||||
def test_long_description(self):
|
||||
desc = "描述" * 200
|
||||
t = EditTemplate.create(name="test", description=desc)
|
||||
assert t.description == desc
|
||||
assert len(t.description) == 400
|
||||
|
||||
def test_unicode_name(self):
|
||||
t = EditTemplate.create(name="🎬 口播 Vlog 模板")
|
||||
assert "🎬" in t.name
|
||||
assert "口播" in t.name
|
||||
|
||||
def test_special_characters_name(self):
|
||||
special = "模!@#$%板"
|
||||
t = EditTemplate.create(name=special)
|
||||
assert t.name == special
|
||||
|
||||
def test_long_name(self):
|
||||
long_name = "模板名称" * 50
|
||||
t = EditTemplate.create(name=long_name)
|
||||
assert t.name == long_name
|
||||
assert len(t.name) == 200
|
||||
|
||||
def test_config_independence(self):
|
||||
t1 = EditTemplate.create(name="test1")
|
||||
t2 = EditTemplate.create(name="test2")
|
||||
t1.config["key"] = "val"
|
||||
assert "key" not in t2.config
|
||||
|
||||
def test_sort_weight_negative(self):
|
||||
t = EditTemplate.create(name="test", sort_weight=-100)
|
||||
assert t.sort_weight == -100
|
||||
|
||||
def test_sort_weight_large(self):
|
||||
t = EditTemplate.create(name="test", sort_weight=99999)
|
||||
assert t.sort_weight == 99999
|
||||
|
||||
def test_sort_weight_zero(self):
|
||||
t = EditTemplate.create(name="test", sort_weight=0)
|
||||
assert t.sort_weight == 0
|
||||
|
||||
def test_preview_url_empty(self):
|
||||
t = EditTemplate.create(name="test", preview_url="")
|
||||
assert t.preview_url == ""
|
||||
|
||||
def test_version_zero(self):
|
||||
t = EditTemplate.create(name="test", version=0)
|
||||
assert t.version == 0
|
||||
|
||||
def test_version_large(self):
|
||||
t = EditTemplate.create(name="test", version=999)
|
||||
assert t.version == 999
|
||||
|
||||
def test_status_is_active_property(self):
|
||||
t = EditTemplate.create(name="test", status=EditTemplateStatus.ACTIVE)
|
||||
assert t.is_active is True
|
||||
t.deactivate()
|
||||
assert t.is_active is False
|
||||
t.activate()
|
||||
assert t.is_active is True
|
||||
|
||||
def test_bump_version_from_zero(self):
|
||||
t = EditTemplate.create(name="test", version=0)
|
||||
t.bump_version()
|
||||
assert t.version == 1
|
||||
|
||||
def test_template_type_custom(self):
|
||||
t = EditTemplate.create(name="test", template_type="custom_type")
|
||||
assert t.template_type == "custom_type"
|
||||
|
||||
def test_template_type_strips_and_default(self):
|
||||
"""空格的 template_type 回退到 default"""
|
||||
t = EditTemplate.create(name="test", template_type=" ")
|
||||
assert t.template_type == "default"
|
||||
|
||||
def test_create_with_empty_editing_mode_defaults(self):
|
||||
"""空字符串 editing_mode 回退到 one_take"""
|
||||
t = EditTemplate.create(name="test", editing_mode="")
|
||||
assert t.editing_mode == "one_take"
|
||||
|
||||
def test_preview_url_strips_whitespace(self):
|
||||
t = EditTemplate.create(name="test", preview_url=" https://example.com/v.mp4 ")
|
||||
assert t.preview_url == "https://example.com/v.mp4"
|
||||
|
||||
@@ -38,3 +38,57 @@ class TestEditingMode:
|
||||
modes = list(EditingMode)
|
||||
assert len(modes) == 4
|
||||
assert EditingMode.ONE_TAKE in modes
|
||||
|
||||
|
||||
class TestEditingModeExtended:
|
||||
"""EditingMode 深度补充测试"""
|
||||
|
||||
def test_from_string_value(self):
|
||||
"""可以从字符串值构造枚举"""
|
||||
assert EditingMode("one_take") == EditingMode.ONE_TAKE
|
||||
assert EditingMode("pip") == EditingMode.PIP
|
||||
assert EditingMode("voice_over") == EditingMode.VOICE_OVER
|
||||
assert EditingMode("voice_pip") == EditingMode.VOICE_PIP
|
||||
|
||||
def test_invalid_string_raises(self):
|
||||
import pytest
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
EditingMode("invalid_mode")
|
||||
|
||||
def test_string_concatenation(self):
|
||||
"""StrEnum 支持字符串拼接"""
|
||||
result = "mode_" + EditingMode.ONE_TAKE
|
||||
assert result == "mode_one_take"
|
||||
|
||||
def test_dict_key_usage(self):
|
||||
"""可以作为字典 key 使用"""
|
||||
mapping = {
|
||||
EditingMode.ONE_TAKE: "顺序拼接",
|
||||
EditingMode.PIP: "画中画",
|
||||
}
|
||||
assert mapping[EditingMode.ONE_TAKE] == "顺序拼接"
|
||||
assert mapping[EditingMode.PIP] == "画中画"
|
||||
assert len(mapping) == 2
|
||||
|
||||
def test_value_lowercase(self):
|
||||
"""所有枚举值都是小写字母+下划线"""
|
||||
for mode in EditingMode:
|
||||
assert mode.value == mode.value.lower()
|
||||
assert " " not in mode.value
|
||||
|
||||
def test_unique_values(self):
|
||||
"""所有枚举值唯一"""
|
||||
values = [m.value for m in EditingMode]
|
||||
assert len(values) == len(set(values))
|
||||
|
||||
def test_membership_test(self):
|
||||
assert EditingMode.ONE_TAKE in EditingMode
|
||||
assert "one_take" in [m.value for m in EditingMode]
|
||||
|
||||
def test_comparison_with_string(self):
|
||||
"""和字符串直接比较"""
|
||||
mode = EditingMode.VOICE_OVER
|
||||
assert mode == "voice_over"
|
||||
assert mode != "pip"
|
||||
assert "voice_over" == mode
|
||||
|
||||
@@ -82,3 +82,102 @@ class TestRecipe:
|
||||
for itype in ["asset", "title", "voice"]:
|
||||
item = RecipeItem(id="i1", recipe_id="r1", item_type=itype, item_id="x", position=0)
|
||||
assert item.item_type == itype
|
||||
|
||||
def test_item_metadata_independence(self):
|
||||
"""不同 RecipeItem 的 metadata_ 互不影响"""
|
||||
item1 = RecipeItem(id="i1", recipe_id="r1", item_type="asset", item_id="a1")
|
||||
item2 = RecipeItem(id="i2", recipe_id="r1", item_type="asset", item_id="a2")
|
||||
item1.metadata_["key"] = "val"
|
||||
assert "key" not in item2.metadata_
|
||||
|
||||
def test_item_position_negative(self):
|
||||
"""负数 position 也能存"""
|
||||
item = RecipeItem(id="i1", recipe_id="r1", item_type="asset", item_id="a1", position=-5)
|
||||
assert item.position == -5
|
||||
|
||||
def test_item_position_large(self):
|
||||
item = RecipeItem(id="i1", recipe_id="r1", item_type="asset", item_id="a1", position=9999)
|
||||
assert item.position == 9999
|
||||
|
||||
def test_item_empty_item_id(self):
|
||||
item = RecipeItem(id="i1", recipe_id="r1", item_type="title", item_id="")
|
||||
assert item.item_id == ""
|
||||
|
||||
def test_item_type_voice(self):
|
||||
item = RecipeItem(id="i1", recipe_id="r1", item_type="voice", item_id="v1")
|
||||
assert item.item_type == "voice"
|
||||
|
||||
def test_item_type_title(self):
|
||||
item = RecipeItem(id="i1", recipe_id="r1", item_type="title", item_id="t1")
|
||||
assert item.item_type == "title"
|
||||
|
||||
|
||||
class TestRecipeExtended:
|
||||
"""Recipe 深度补充测试"""
|
||||
|
||||
def test_items_order_preserved(self):
|
||||
items = [
|
||||
RecipeItem(id="i3", recipe_id="r1", item_type="asset", item_id="a1", position=2),
|
||||
RecipeItem(id="i1", recipe_id="r1", item_type="title", item_id="t1", position=0),
|
||||
RecipeItem(id="i2", recipe_id="r1", item_type="voice", item_id="v1", position=1),
|
||||
]
|
||||
r = Recipe(id="r1", user_id="u1", name="n", items=items)
|
||||
assert len(r.items) == 3
|
||||
assert r.items[0].position == 2
|
||||
assert r.items[1].position == 0
|
||||
assert r.items[2].position == 1
|
||||
|
||||
def test_empty_items_list(self):
|
||||
r = Recipe(id="r1", user_id="u1", name="n", items=[])
|
||||
assert r.items == []
|
||||
|
||||
def test_many_items(self):
|
||||
items = [
|
||||
RecipeItem(id=f"i{i}", recipe_id="r1", item_type="asset", item_id=f"a{i}", position=i) for i in range(50)
|
||||
]
|
||||
r = Recipe(id="r1", user_id="u1", name="n", items=items)
|
||||
assert len(r.items) == 50
|
||||
assert r.items[0].position == 0
|
||||
assert r.items[49].position == 49
|
||||
|
||||
def test_generation_params_independence(self):
|
||||
params = {"mode": "one_take", "duration": 30}
|
||||
r1 = Recipe(id="r1", user_id="u1", name="n1", generation_params=params)
|
||||
r2 = Recipe(id="r2", user_id="u1", name="n2")
|
||||
r1.generation_params["new_key"] = "new_val"
|
||||
# 传入同一个 dict 会共享,但默认生成的互不影响
|
||||
assert r2.generation_params == {}
|
||||
|
||||
def test_metadata_independence_default(self):
|
||||
r1 = Recipe(id="r1", user_id="u1", name="n1")
|
||||
r2 = Recipe(id="r2", user_id="u1", name="n2")
|
||||
r1.metadata_["key"] = "val"
|
||||
assert "key" not in r2.metadata_
|
||||
|
||||
def test_with_description(self):
|
||||
r = Recipe(id="r1", user_id="u1", name="n", description="这是一个测试配方")
|
||||
assert r.description == "这是一个测试配方"
|
||||
|
||||
def test_with_template_id(self):
|
||||
r = Recipe(id="r1", user_id="u1", name="n", template_id="tpl-123")
|
||||
assert r.template_id == "tpl-123"
|
||||
|
||||
def test_empty_name(self):
|
||||
r = Recipe(id="r1", user_id="u1", name="")
|
||||
assert r.name == ""
|
||||
|
||||
def test_long_name(self):
|
||||
long_name = "配方" * 200
|
||||
r = Recipe(id="r1", user_id="u1", name=long_name)
|
||||
assert r.name == long_name
|
||||
assert len(r.name) == 400
|
||||
|
||||
def test_special_characters_in_name(self):
|
||||
special = "配!@#$%方"
|
||||
r = Recipe(id="r1", user_id="u1", name=special)
|
||||
assert r.name == special
|
||||
|
||||
def test_unicode_name(self):
|
||||
r = Recipe(id="r1", user_id="u1", name="🎬 一键生成配方 · 美食探店")
|
||||
assert "🎬" in r.name
|
||||
assert "美食探店" in r.name
|
||||
|
||||
@@ -32,3 +32,61 @@ class TestTagCreate:
|
||||
def test_create_has_created_at(self):
|
||||
tag = Tag.create(user_id="user-1", name="美食")
|
||||
assert tag.created_at is not None
|
||||
|
||||
|
||||
class TestTagExtended:
|
||||
"""Tag 深度补充测试"""
|
||||
|
||||
def test_create_id_is_unique(self):
|
||||
"""多次创建生成不同的 id"""
|
||||
tag1 = Tag.create(user_id="u1", name="标签1")
|
||||
tag2 = Tag.create(user_id="u1", name="标签2")
|
||||
assert tag1.id != tag2.id
|
||||
assert len(tag1.id) == 32
|
||||
assert len(tag2.id) == 32
|
||||
|
||||
def test_create_id_is_hex(self):
|
||||
"""id 是十六进制字符串"""
|
||||
tag = Tag.create(user_id="u1", name="测试")
|
||||
int(tag.id, 16) # 不抛错就是合法 hex
|
||||
|
||||
def test_slots_no_extra_attributes(self):
|
||||
"""slots=True 不能添加新属性"""
|
||||
import pytest
|
||||
|
||||
tag = Tag.create(user_id="u1", name="测试")
|
||||
with pytest.raises(AttributeError):
|
||||
tag.new_attr = "value" # type: ignore[attr-defined]
|
||||
|
||||
def test_create_very_long_name(self):
|
||||
long_name = "标签" * 50
|
||||
tag = Tag.create(user_id="u1", name=long_name)
|
||||
assert tag.name == long_name
|
||||
assert len(tag.name) == 100
|
||||
|
||||
def test_create_unicode_name(self):
|
||||
tag = Tag.create(user_id="u1", name="🔥热门标签✨")
|
||||
assert tag.name == "🔥热门标签✨"
|
||||
|
||||
def test_create_name_only_spaces_between_text(self):
|
||||
"""中间有空格的标签名正常保留"""
|
||||
tag = Tag.create(user_id="u1", name=" 美食 探店 ")
|
||||
assert tag.name == "美食 探店"
|
||||
|
||||
def test_create_different_user_ids(self):
|
||||
tag1 = Tag.create(user_id="user-001", name="标签")
|
||||
tag2 = Tag.create(user_id="user-999", name="标签")
|
||||
assert tag1.user_id == "user-001"
|
||||
assert tag2.user_id == "user-999"
|
||||
assert tag1.id != tag2.id
|
||||
|
||||
def test_name_is_string(self):
|
||||
tag = Tag.create(user_id="u1", name="12345")
|
||||
assert isinstance(tag.name, str)
|
||||
assert tag.name == "12345"
|
||||
|
||||
def test_equality(self):
|
||||
"""两个相同属性的 tag 不相等(id 不同)"""
|
||||
tag1 = Tag.create(user_id="u1", name="同名")
|
||||
tag2 = Tag.create(user_id="u1", name="同名")
|
||||
assert tag1 != tag2
|
||||
|
||||
Regular → Executable
+139
@@ -151,3 +151,142 @@ class TestTemplateCategory:
|
||||
def test_category_has_timestamp(self):
|
||||
cat = TemplateCategory(id="cat-1", user_id="u1", name="风景")
|
||||
assert cat.created_at is not None
|
||||
|
||||
|
||||
class TestTemplateExtended:
|
||||
"""Template 深度补充测试"""
|
||||
|
||||
def test_tags_independence(self):
|
||||
"""不同模板的 tags 列表互不影响"""
|
||||
t1 = Template(id="t1", user_id="u1", name="n1", mode="one_take")
|
||||
t2 = Template(id="t2", user_id="u1", name="n2", mode="one_take")
|
||||
t1.tags.append("新标签")
|
||||
assert "新标签" not in t2.tags
|
||||
assert len(t2.tags) == 0
|
||||
|
||||
def test_title_config_independence(self):
|
||||
t1 = Template(id="t1", user_id="u1", name="n1", mode="one_take")
|
||||
t2 = Template(id="t2", user_id="u1", name="n2", mode="one_take")
|
||||
t1.title_config["key"] = "val"
|
||||
assert "key" not in t2.title_config
|
||||
|
||||
def test_subtitle_config_independence(self):
|
||||
t1 = Template(id="t1", user_id="u1", name="n1", mode="one_take")
|
||||
t2 = Template(id="t2", user_id="u1", name="n2", mode="one_take")
|
||||
t1.subtitle_config["key"] = "val"
|
||||
assert "key" not in t2.subtitle_config
|
||||
|
||||
def test_bgm_config_independence(self):
|
||||
t1 = Template(id="t1", user_id="u1", name="n1", mode="one_take")
|
||||
t2 = Template(id="t2", user_id="u1", name="n2", mode="one_take")
|
||||
t1.bgm_config["key"] = "val"
|
||||
assert "key" not in t2.bgm_config
|
||||
|
||||
def test_segments_independence(self):
|
||||
segs = [
|
||||
TemplateSegment(id="s1", template_id="t1", segment_order=0, duration_min=1, duration_max=2),
|
||||
]
|
||||
t1 = Template(id="t1", user_id="u1", name="n1", mode="one_take", segments=segs)
|
||||
t2 = Template(id="t2", user_id="u1", name="n2", mode="one_take")
|
||||
assert len(t2.segments) == 0
|
||||
|
||||
def test_empty_segments(self):
|
||||
t = Template(id="t1", user_id="u1", name="n", mode="one_take", segments=[])
|
||||
assert t.segments == []
|
||||
|
||||
def test_many_segments(self):
|
||||
segs = [
|
||||
TemplateSegment(id=f"s{i}", template_id="t1", segment_order=i, duration_min=1, duration_max=3)
|
||||
for i in range(30)
|
||||
]
|
||||
t = Template(id="t1", user_id="u1", name="n", mode="one_take", segments=segs)
|
||||
assert len(t.segments) == 30
|
||||
assert t.segments[0].segment_order == 0
|
||||
assert t.segments[29].segment_order == 29
|
||||
|
||||
def test_zero_estimated_duration(self):
|
||||
t = Template(id="t1", user_id="u1", name="n", mode="one_take", estimated_duration=0.0)
|
||||
assert t.estimated_duration == 0.0
|
||||
|
||||
def test_negative_estimated_duration(self):
|
||||
t = Template(id="t1", user_id="u1", name="n", mode="one_take", estimated_duration=-1.0)
|
||||
assert t.estimated_duration == -1.0
|
||||
|
||||
def test_large_estimated_duration(self):
|
||||
t = Template(id="t1", user_id="u1", name="n", mode="one_take", estimated_duration=9999.9)
|
||||
assert t.estimated_duration == 9999.9
|
||||
|
||||
def test_empty_category(self):
|
||||
t = Template(id="t1", user_id="u1", name="n", mode="one_take", category="")
|
||||
assert t.category == ""
|
||||
|
||||
def test_custom_category(self):
|
||||
t = Template(id="t1", user_id="u1", name="n", mode="one_take", category="美食探店")
|
||||
assert t.category == "美食探店"
|
||||
|
||||
def test_empty_name(self):
|
||||
t = Template(id="t1", user_id="u1", name="", mode="one_take")
|
||||
assert t.name == ""
|
||||
|
||||
def test_unicode_name(self):
|
||||
t = Template(id="t1", user_id="u1", name="🎬 美食探店 · Vlog模板", mode="one_take")
|
||||
assert "🎬" in t.name
|
||||
assert "美食探店" in t.name
|
||||
|
||||
def test_special_characters_in_name(self):
|
||||
special = "模!@#$%板"
|
||||
t = Template(id="t1", user_id="u1", name=special, mode="one_take")
|
||||
assert t.name == special
|
||||
|
||||
def test_long_name(self):
|
||||
long_name = "模板" * 100
|
||||
t = Template(id="t1", user_id="u1", name=long_name, mode="one_take")
|
||||
assert t.name == long_name
|
||||
assert len(t.name) == 200
|
||||
|
||||
|
||||
class TestTemplateSegmentExtended:
|
||||
"""TemplateSegment 深度补充测试"""
|
||||
|
||||
def test_zero_duration(self):
|
||||
seg = TemplateSegment(id="s1", template_id="t1", segment_order=0, duration_min=0, duration_max=0)
|
||||
assert seg.duration_min == 0
|
||||
assert seg.duration_max == 0
|
||||
|
||||
def test_negative_duration_min(self):
|
||||
seg = TemplateSegment(id="s1", template_id="t1", segment_order=0, duration_min=-1, duration_max=5)
|
||||
assert seg.duration_min == -1
|
||||
|
||||
def test_negative_duration_max(self):
|
||||
seg = TemplateSegment(id="s1", template_id="t1", segment_order=0, duration_min=1, duration_max=-5)
|
||||
assert seg.duration_max == -5
|
||||
|
||||
def test_large_duration(self):
|
||||
seg = TemplateSegment(id="s1", template_id="t1", segment_order=0, duration_min=0, duration_max=9999.9)
|
||||
assert seg.duration_max == 9999.9
|
||||
|
||||
def test_negative_order(self):
|
||||
seg = TemplateSegment(id="s1", template_id="t1", segment_order=-5, duration_min=1, duration_max=3)
|
||||
assert seg.segment_order == -5
|
||||
|
||||
def test_large_order(self):
|
||||
seg = TemplateSegment(id="s1", template_id="t1", segment_order=999, duration_min=1, duration_max=3)
|
||||
assert seg.segment_order == 999
|
||||
|
||||
def test_material_type_none(self):
|
||||
seg = TemplateSegment(
|
||||
id="s1", template_id="t1", segment_order=0, duration_min=1, duration_max=3, material_type=None
|
||||
)
|
||||
assert seg.material_type is None
|
||||
|
||||
def test_material_type_empty_string(self):
|
||||
seg = TemplateSegment(
|
||||
id="s1", template_id="t1", segment_order=0, duration_min=1, duration_max=3, material_type=""
|
||||
)
|
||||
assert seg.material_type == ""
|
||||
|
||||
def test_material_type_unicode(self):
|
||||
seg = TemplateSegment(
|
||||
id="s1", template_id="t1", segment_order=0, duration_min=1, duration_max=3, material_type="风景"
|
||||
)
|
||||
assert seg.material_type == "风景"
|
||||
|
||||
Regular → Executable
+78
@@ -125,3 +125,81 @@ class TestEditTemplateVersionSlots:
|
||||
v = EditTemplateVersion.create(template_id="t1", version=1)
|
||||
with pytest.raises(AttributeError):
|
||||
v.nonexistent_field = "value" # type: ignore[attr-defined]
|
||||
|
||||
|
||||
class TestEditTemplateVersionExtended:
|
||||
"""EditTemplateVersion 深度补充测试"""
|
||||
|
||||
def test_id_is_hex(self):
|
||||
"""id 是十六进制字符串"""
|
||||
v = EditTemplateVersion.create(template_id="t1", version=1)
|
||||
int(v.id, 16) # 不抛错就是合法 hex
|
||||
|
||||
def test_ids_are_unique(self):
|
||||
"""不同版本的 id 不同"""
|
||||
v1 = EditTemplateVersion.create(template_id="t1", version=1)
|
||||
v2 = EditTemplateVersion.create(template_id="t1", version=2)
|
||||
assert v1.id != v2.id
|
||||
|
||||
def test_zero_version(self):
|
||||
"""version=0 也能创建"""
|
||||
v = EditTemplateVersion.create(template_id="t1", version=0)
|
||||
assert v.version == 0
|
||||
|
||||
def test_negative_version(self):
|
||||
"""负数 version 也能创建(领域层不做业务校验)"""
|
||||
v = EditTemplateVersion.create(template_id="t1", version=-1)
|
||||
assert v.version == -1
|
||||
|
||||
def test_large_version(self):
|
||||
v = EditTemplateVersion.create(template_id="t1", version=9999)
|
||||
assert v.version == 9999
|
||||
|
||||
def test_empty_template_id(self):
|
||||
v = EditTemplateVersion.create(template_id="", version=1)
|
||||
assert v.template_id == ""
|
||||
|
||||
def test_empty_name(self):
|
||||
v = EditTemplateVersion.create(template_id="t1", version=1, name="")
|
||||
assert v.name == ""
|
||||
|
||||
def test_empty_change_note(self):
|
||||
v = EditTemplateVersion.create(template_id="t1", version=1, change_note="")
|
||||
assert v.change_note == ""
|
||||
|
||||
def test_empty_published_by(self):
|
||||
v = EditTemplateVersion.create(template_id="t1", version=1, published_by="")
|
||||
assert v.published_by == ""
|
||||
|
||||
def test_unicode_name(self):
|
||||
v = EditTemplateVersion.create(template_id="t1", version=1, name="🎨 V5 优化版")
|
||||
assert "🎨" in v.name
|
||||
assert "V5" in v.name
|
||||
|
||||
def test_long_name(self):
|
||||
long_name = "版本" * 50
|
||||
v = EditTemplateVersion.create(template_id="t1", version=1, name=long_name)
|
||||
assert v.name == long_name
|
||||
assert len(v.name) == 100
|
||||
|
||||
def test_config_complex_nested(self):
|
||||
config = {
|
||||
"layer1": {
|
||||
"layer2": {
|
||||
"layer3": [1, 2, 3],
|
||||
}
|
||||
}
|
||||
}
|
||||
v = EditTemplateVersion.create(template_id="t1", version=1, config=config)
|
||||
assert v.config["layer1"]["layer2"]["layer3"] == [1, 2, 3]
|
||||
|
||||
def test_clip_configs_empty_list(self):
|
||||
v = EditTemplateVersion.create(template_id="t1", version=1, clip_configs=[])
|
||||
assert v.clip_configs == []
|
||||
|
||||
def test_clip_configs_many(self):
|
||||
clips = [{"clip_id": i, "duration": float(i)} for i in range(50)]
|
||||
v = EditTemplateVersion.create(template_id="t1", version=1, clip_configs=clips)
|
||||
assert len(v.clip_configs) == 50
|
||||
assert v.clip_configs[0]["clip_id"] == 0
|
||||
assert v.clip_configs[49]["clip_id"] == 49
|
||||
|
||||
@@ -76,3 +76,84 @@ class TestTitleLibraryItem:
|
||||
item = TitleLibraryItem(id="t1", user_id="u1", name="n", text="t")
|
||||
assert item.created_at is not None
|
||||
assert item.updated_at is not None
|
||||
|
||||
def test_tags_independence_between_instances(self):
|
||||
"""不同实例的 tags 列表互不影响"""
|
||||
item1 = TitleLibraryItem(id="t1", user_id="u1", name="n1", text="t")
|
||||
item2 = TitleLibraryItem(id="t2", user_id="u1", name="n2", text="t")
|
||||
item1.tags.append("新标签")
|
||||
assert "新标签" not in item2.tags
|
||||
assert len(item2.tags) == 0
|
||||
|
||||
def test_metadata_independence_between_instances(self):
|
||||
"""不同实例的 metadata_ 字典互不影响"""
|
||||
item1 = TitleLibraryItem(id="t1", user_id="u1", name="n1", text="t")
|
||||
item2 = TitleLibraryItem(id="t2", user_id="u1", name="n2", text="t")
|
||||
item1.metadata_["key"] = "value"
|
||||
assert "key" not in item2.metadata_
|
||||
|
||||
def test_zero_usage_count(self):
|
||||
item = TitleLibraryItem(id="t1", user_id="u1", name="n", text="t", usage_count=0)
|
||||
assert item.usage_count == 0
|
||||
|
||||
def test_large_usage_count(self):
|
||||
item = TitleLibraryItem(id="t1", user_id="u1", name="n", text="t", usage_count=999999)
|
||||
assert item.usage_count == 999999
|
||||
|
||||
def test_negative_usage_count(self):
|
||||
"""负数使用次数也能存(领域层不做业务校验)"""
|
||||
item = TitleLibraryItem(id="t1", user_id="u1", name="n", text="t", usage_count=-1)
|
||||
assert item.usage_count == -1
|
||||
|
||||
def test_empty_text(self):
|
||||
item = TitleLibraryItem(id="t1", user_id="u1", name="n", text="")
|
||||
assert item.text == ""
|
||||
|
||||
def test_long_text(self):
|
||||
long_text = "标题" * 1000
|
||||
item = TitleLibraryItem(id="t1", user_id="u1", name="n", text=long_text)
|
||||
assert item.text == long_text
|
||||
assert len(item.text) == 2000
|
||||
|
||||
def test_empty_name(self):
|
||||
item = TitleLibraryItem(id="t1", user_id="u1", name="", text="t")
|
||||
assert item.name == ""
|
||||
|
||||
def test_special_characters_in_name_and_text(self):
|
||||
special = "!@#$%^&*()_+-=[]{}|;':\",./<>?\n\t"
|
||||
item = TitleLibraryItem(id="t1", user_id="u1", name=special, text=special)
|
||||
assert item.name == special
|
||||
assert item.text == special
|
||||
|
||||
def test_unicode_in_name_and_text(self):
|
||||
item = TitleLibraryItem(
|
||||
id="t1",
|
||||
user_id="u1",
|
||||
name="中文标题🔥emoji",
|
||||
text="支持各种文字:中文、English、日本語、한국어",
|
||||
)
|
||||
assert "中文" in item.name
|
||||
assert "🔥" in item.name
|
||||
assert "日本語" in item.text
|
||||
|
||||
def test_many_tags(self):
|
||||
tags = [f"tag_{i}" for i in range(100)]
|
||||
item = TitleLibraryItem(id="t1", user_id="u1", name="n", text="t", tags=tags)
|
||||
assert len(item.tags) == 100
|
||||
assert item.tags[0] == "tag_0"
|
||||
assert item.tags[99] == "tag_99"
|
||||
|
||||
def test_is_active_toggle(self):
|
||||
item = TitleLibraryItem(id="t1", user_id="u1", name="n", text="t", is_active=True)
|
||||
item.is_active = False
|
||||
assert item.is_active is False
|
||||
item.is_active = True
|
||||
assert item.is_active is True
|
||||
|
||||
def test_empty_description(self):
|
||||
item = TitleLibraryItem(id="t1", user_id="u1", name="n", text="t", description="")
|
||||
assert item.description == ""
|
||||
|
||||
def test_custom_category(self):
|
||||
item = TitleLibraryItem(id="t1", user_id="u1", name="n", text="t", category="自定义分类")
|
||||
assert item.category == "自定义分类"
|
||||
|
||||
@@ -93,3 +93,93 @@ class TestVoiceLibraryItem:
|
||||
item = VoiceLibraryItem(id="v1", user_id="u1", name="n")
|
||||
assert item.created_at is not None
|
||||
assert item.updated_at is not None
|
||||
|
||||
|
||||
class TestVoiceLibraryItemExtended:
|
||||
"""VoiceLibraryItem 深度补充测试"""
|
||||
|
||||
def test_zero_duration(self):
|
||||
item = VoiceLibraryItem(id="v1", user_id="u1", name="n", duration=0)
|
||||
assert item.duration == 0
|
||||
|
||||
def test_negative_duration(self):
|
||||
"""负数 duration 领域层不校验"""
|
||||
item = VoiceLibraryItem(id="v1", user_id="u1", name="n", duration=-1.5)
|
||||
assert item.duration == -1.5
|
||||
|
||||
def test_large_duration(self):
|
||||
item = VoiceLibraryItem(id="v1", user_id="u1", name="n", duration=9999.99)
|
||||
assert item.duration == 9999.99
|
||||
|
||||
def test_zero_file_size(self):
|
||||
item = VoiceLibraryItem(id="v1", user_id="u1", name="n", file_size=0)
|
||||
assert item.file_size == 0
|
||||
|
||||
def test_large_file_size(self):
|
||||
item = VoiceLibraryItem(id="v1", user_id="u1", name="n", file_size=10**9)
|
||||
assert item.file_size == 10**9
|
||||
|
||||
def test_empty_audio_url(self):
|
||||
item = VoiceLibraryItem(id="v1", user_id="u1", name="n", audio_url="")
|
||||
assert item.audio_url == ""
|
||||
|
||||
def test_tags_independence(self):
|
||||
item1 = VoiceLibraryItem(id="v1", user_id="u1", name="n1")
|
||||
item2 = VoiceLibraryItem(id="v2", user_id="u1", name="n2")
|
||||
item1.tags.append("新标签")
|
||||
assert "新标签" not in item2.tags
|
||||
assert len(item2.tags) == 0
|
||||
|
||||
def test_metadata_independence(self):
|
||||
item1 = VoiceLibraryItem(id="v1", user_id="u1", name="n1")
|
||||
item2 = VoiceLibraryItem(id="v2", user_id="u1", name="n2")
|
||||
item1.metadata_["key"] = "val"
|
||||
assert "key" not in item2.metadata_
|
||||
|
||||
def test_many_tags(self):
|
||||
tags = [f"tag_{i}" for i in range(30)]
|
||||
item = VoiceLibraryItem(id="v1", user_id="u1", name="n", tags=tags)
|
||||
assert len(item.tags) == 30
|
||||
assert item.tags[0] == "tag_0"
|
||||
|
||||
def test_empty_text(self):
|
||||
item = VoiceLibraryItem(id="v1", user_id="u1", name="n", text="")
|
||||
assert item.text == ""
|
||||
|
||||
def test_long_text(self):
|
||||
long_text = "这是一段很长的配音文本" * 100
|
||||
item = VoiceLibraryItem(id="v1", user_id="u1", name="n", text=long_text)
|
||||
assert item.text == long_text
|
||||
assert len(item.text) == 1100
|
||||
|
||||
def test_empty_voice_id(self):
|
||||
item = VoiceLibraryItem(id="v1", user_id="u1", name="n", voice_id="")
|
||||
assert item.voice_id == ""
|
||||
|
||||
def test_empty_project_id(self):
|
||||
"""project_id 默认是 None 不是空字符串"""
|
||||
item = VoiceLibraryItem(id="v1", user_id="u1", name="n")
|
||||
assert item.project_id is None
|
||||
|
||||
def test_project_id_with_string(self):
|
||||
item = VoiceLibraryItem(id="v1", user_id="u1", name="n", project_id="")
|
||||
# 传空字符串的话就是空字符串
|
||||
assert item.project_id == ""
|
||||
|
||||
def test_status_empty_string(self):
|
||||
item = VoiceLibraryItem(id="v1", user_id="u1", name="n", status="")
|
||||
assert item.status == ""
|
||||
|
||||
def test_unicode_name(self):
|
||||
item = VoiceLibraryItem(id="v1", user_id="u1", name="🎙️ 专业配音 · 龙小淳")
|
||||
assert "🎙️" in item.name
|
||||
assert "龙小淳" in item.name
|
||||
|
||||
def test_special_characters_in_name(self):
|
||||
special = "配!@#$%^&*()音"
|
||||
item = VoiceLibraryItem(id="v1", user_id="u1", name=special)
|
||||
assert item.name == special
|
||||
|
||||
def test_empty_user_id(self):
|
||||
item = VoiceLibraryItem(id="v1", user_id="", name="n")
|
||||
assert item.user_id == ""
|
||||
|
||||
Reference in New Issue
Block a user