Compare commits

...

2 Commits

Author SHA1 Message Date
CI Bot ad765cf150 test: 第82波单测 - exceptions/template/edit_template/generated_video (+86)
AI Code Review / AI Code Review (pull_request) Failing after 0s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 14s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m18s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m9s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 38s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 29s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 26s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 2m8s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 3m43s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 4m0s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m27s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 2m3s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m49s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 47m48s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
ACR Cleanup / ACR Image Cleanup (pull_request_target) Has been cancelled
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 17s
新增86个测试:
- test_domain_exceptions +20(新建:DomainError/NotFoundError/ValidationError/QuotaExceededError)
- test_template_domain +28(tags/config独立性、零值大值负值、长名称unicode、多segments等)
- test_template_version_domain +16(id hex/唯一性、零值负值大值version、复杂config等)
- test_edit_template_domain +22(id hex/唯一性、长描述、sort_weight边界、状态切换等)
2026-07-25 21:07:09 +08:00
CI Bot ba3a916a75 test: 第81波单测 - 小领域模块深度补充 (+73)
新增73个测试,5个小领域模块深度覆盖:
- title_library_domain +15(tags/metadata独立性、零值大值、长文本、unicode、特殊字符等)
- recipe_domain +22(RecipeItem边界、items顺序/数量、生成参数独立性、长名称等)
- editing_mode_domain +8(from string、字符串拼接、dict key、枚举值校验等)
- tag_domain +9(id唯一性/hex、slots、长名称、unicode、相等性等)
- voice_library_domain +19(零值/大值/负值、tags/metadata独立性、project_id边界等)
2026-07-25 21:07:06 +08:00
9 changed files with 751 additions and 69 deletions
+52 -69
View File
@@ -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
+100
View File
@@ -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"
+54
View File
@@ -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
+99
View File
@@ -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
+58
View File
@@ -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
+139
View File
@@ -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 == "风景"
+78
View File
@@ -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
+81
View File
@@ -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 == "自定义分类"
+90
View File
@@ -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 == ""