Compare commits

...

3 Commits

Author SHA1 Message Date
CI Bot c33a14c3c6 test: 第83波单测 - generated_video/verification/video_share (+55)
AI Code Review / AI Code Review (pull_request) Failing after 0s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 11s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 51s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m3s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 35s
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 / PR Build Worker Image (pull_request) Successful in 18s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m1s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m53s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 18s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 4m27s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m9s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 2m7s
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 / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m17s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
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
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 47m39s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 21s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Failing after 21s
新增55个测试:
- generated_video +22(零值大值4K、unicode特殊字符、params独立性、状态切换等)
- verification_code +16(id hex/唯一性、slots、自定义code边界、ttl边界等)
- video_share +17(token边界长度、字母表校验、大计数、密码特殊字符unicode等)
2026-07-25 21:07:59 +08:00
CI Bot e5c271c03a test: 第82波单测 - exceptions/template/edit_template/generated_video (+86)
新增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:57 +08:00
CI Bot f5f09b7048 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:54 +08:00
12 changed files with 1047 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
+114
View File
@@ -194,3 +194,117 @@ class TestGeneratedVideoProperties:
fingerprint = {"phash": "abc123", "md5": "def456"}
gv.video_fingerprint = fingerprint
assert gv.video_fingerprint == fingerprint
class TestGeneratedVideoExtended:
"""GeneratedVideo 深度补充测试"""
def test_id_is_hex(self):
v = GeneratedVideo.create(project_id="p", generation_task_id="t", name="n", file_url="u")
int(v.id, 16)
def test_zero_file_size(self):
v = GeneratedVideo.create(project_id="p", generation_task_id="t", name="n", file_url="u", file_size=0)
assert v.file_size == 0
def test_large_file_size(self):
large = 1024 * 1024 * 1024 # 1GB
v = GeneratedVideo.create(project_id="p", generation_task_id="t", name="n", file_url="u", file_size=large)
assert v.file_size == large
def test_zero_duration(self):
v = GeneratedVideo.create(project_id="p", generation_task_id="t", name="n", file_url="u", duration=0.0)
assert v.duration == 0.0
def test_large_duration(self):
v = GeneratedVideo.create(project_id="p", generation_task_id="t", name="n", file_url="u", duration=9999.99)
assert v.duration == 9999.99
def test_zero_dimensions(self):
v = GeneratedVideo.create(project_id="p", generation_task_id="t", name="n", file_url="u", width=0, height=0)
assert v.width == 0
assert v.height == 0
def test_4k_dimensions(self):
v = GeneratedVideo.create(
project_id="p", generation_task_id="t", name="n", file_url="u", width=3840, height=2160
)
assert v.width == 3840
assert v.height == 2160
def test_zero_fps(self):
v = GeneratedVideo.create(project_id="p", generation_task_id="t", name="n", file_url="u", fps=0.0)
assert v.fps == 0.0
def test_high_fps(self):
v = GeneratedVideo.create(project_id="p", generation_task_id="t", name="n", file_url="u", fps=120.0)
assert v.fps == 120.0
def test_empty_user_id(self):
v = GeneratedVideo.create(project_id="p", generation_task_id="t", name="n", file_url="u", user_id="")
assert v.user_id == ""
def test_long_name(self):
long_name = "视频" * 100
v = GeneratedVideo.create(project_id="p", generation_task_id="t", name=long_name, file_url="u")
assert v.name == long_name
assert len(v.name) == 200
def test_unicode_name(self):
v = GeneratedVideo.create(
project_id="p", generation_task_id="t", name="🎬 我的精彩视频 · 旅行vlog", file_url="u"
)
assert "🎬" in v.name
assert "旅行vlog" in v.name
def test_special_characters_name(self):
special = "视!@#$%^&*()频"
v = GeneratedVideo.create(project_id="p", generation_task_id="t", name=special, file_url="u")
assert v.name == special
def test_thumbnail_url_none(self):
v = GeneratedVideo.create(project_id="p", generation_task_id="t", name="n", file_url="u", thumbnail_url=None)
assert v.thumbnail_url is None
def test_generation_params_complex(self):
params = {
"mode": "voice_over",
"quality": "high",
"resolution": {"width": 1920, "height": 1080},
"effects": ["filter", "transition"],
}
v = GeneratedVideo.create(
project_id="p", generation_task_id="t", name="n", file_url="u", generation_params=params
)
assert v.generation_params["mode"] == "voice_over"
assert v.generation_params["resolution"]["width"] == 1920
assert len(v.generation_params["effects"]) == 2
def test_generation_params_independence(self):
v1 = GeneratedVideo.create(project_id="p", generation_task_id="t1", name="n1", file_url="u1")
v2 = GeneratedVideo.create(project_id="p", generation_task_id="t2", name="n2", file_url="u2")
v1.generation_params["key"] = "val"
assert "key" not in v2.generation_params
def test_status_failed(self):
v = GeneratedVideo.create(project_id="p", generation_task_id="t", name="n", file_url="u")
v.status = "failed"
assert v.status == "failed"
def test_review_status_approved(self):
v = GeneratedVideo.create(project_id="p", generation_task_id="t", name="n", file_url="u")
v.review_status = "approved"
assert v.review_status == "approved"
def test_is_duplicate_default_false(self):
v = GeneratedVideo.create(project_id="p", generation_task_id="t", name="n", file_url="u")
assert v.is_duplicate is False
assert v.duplicate_of is None
def test_fingerprint_none_default(self):
v = GeneratedVideo.create(project_id="p", generation_task_id="t", name="n", file_url="u")
assert v.video_fingerprint is None
def test_empty_file_url_raises(self):
with pytest.raises(ValueError):
GeneratedVideo.create(project_id="p", generation_task_id="t", name="n", file_url=" ")
+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 == "自定义分类"
+85
View File
@@ -166,3 +166,88 @@ class TestVerificationCodeTypes:
vc = VerificationCode.create("test@example.com", code_type)
assert vc.code_type == code_type
assert vc.is_valid is True
class TestVerificationCodeExtended:
"""VerificationCode 深度补充测试"""
def test_id_is_hex(self):
vc = VerificationCode.create("test@example.com", "email_bind")
int(vc.id, 16)
def test_ids_are_unique(self):
vc1 = VerificationCode.create("test@example.com", "email_bind")
vc2 = VerificationCode.create("test@example.com", "email_bind")
assert vc1.id != vc2.id
def test_slots_no_extra_attrs(self):
import pytest
vc = VerificationCode.create("test@example.com", "email_bind")
with pytest.raises(AttributeError):
vc.new_field = "value" # type: ignore[attr-defined]
def test_custom_code_non_numeric(self):
"""自定义 code 可以是非数字"""
vc = VerificationCode.create("test@example.com", "email_bind", custom_code="abcdef")
assert vc.code == "abcdef"
def test_custom_code_short(self):
vc = VerificationCode.create("test@example.com", "email_bind", custom_code="12")
assert vc.code == "12"
def test_custom_code_long(self):
long_code = "1" * 20
vc = VerificationCode.create("test@example.com", "email_bind", custom_code=long_code)
assert vc.code == long_code
assert len(vc.code) == 20
def test_ttl_zero_expires_immediately(self):
vc = VerificationCode.create("test@example.com", "email_bind", ttl_seconds=0)
# ttl=0 时 expires_at = now,可能已过期或刚好
assert vc.expires_at is not None
def test_very_long_ttl(self):
vc = VerificationCode.create("test@example.com", "email_bind", ttl_seconds=86400)
assert vc.is_expired is False
def test_negative_ttl_expired(self):
vc = VerificationCode.create("test@example.com", "email_bind", ttl_seconds=-100)
assert vc.is_expired is True
def test_attempts_starts_at_zero(self):
vc = VerificationCode.create("test@example.com", "email_bind")
assert vc.attempts == 0
def test_increment_attempts_returns_none(self):
vc = VerificationCode.create("test@example.com", "email_bind")
result = vc.increment_attempts()
assert result is None
assert vc.attempts == 1
def test_mark_used_returns_none(self):
vc = VerificationCode.create("test@example.com", "email_bind")
result = vc.mark_used()
assert result is None
assert vc.is_used is True
def test_code_type_empty_string(self):
vc = VerificationCode.create("test@example.com", "")
assert vc.code_type == ""
def test_recipient_empty_string(self):
vc = VerificationCode.create("", "email_bind")
assert vc.recipient == ""
def test_created_at_equals_expires_minus_ttl(self):
vc = VerificationCode.create("test@example.com", "email_bind", ttl_seconds=60)
diff = (vc.expires_at - vc.created_at).total_seconds()
assert abs(diff - 60) < 2
def test_multiple_mark_used_updates_time(self):
vc = VerificationCode.create("test@example.com", "email_bind")
vc.mark_used()
first = vc.used_at
vc.mark_used()
second = vc.used_at
assert second >= first
+97
View File
@@ -223,3 +223,100 @@ class TestVideoShareCounters:
share.revoke()
assert share.is_active is False
assert share.is_accessible is False
class TestGenerateShareTokenExtended:
"""generate_share_token 深度补充测试"""
def test_zero_length(self):
token = generate_share_token(0)
assert token == ""
def test_length_one(self):
token = generate_share_token(1)
assert len(token) == 1
def test_very_long_token(self):
token = generate_share_token(100)
assert len(token) == 100
def test_no_special_characters(self):
token = generate_share_token(50)
assert token.isalnum()
def test_all_characters_from_alphabet(self):
alphabet = set("abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ23456789")
token = generate_share_token(200)
for c in token:
assert c in alphabet
class TestVideoShareExtended:
"""VideoShare 深度补充测试"""
def test_zero_view_count(self):
share = VideoShare.create(video_id="v1", user_id="u1")
assert share.view_count == 0
def test_zero_download_count(self):
share = VideoShare.create(video_id="v1", user_id="u1")
assert share.download_count == 0
def test_large_view_count(self):
share = VideoShare.create(video_id="v1", user_id="u1")
for _ in range(1000):
share.increment_view_count()
assert share.view_count == 1000
def test_revoke_idempotent(self):
share = VideoShare.create(video_id="v1", user_id="u1")
share.revoke()
assert share.is_active is False
share.revoke()
assert share.is_active is False
def test_revoke_returns_none(self):
share = VideoShare.create(video_id="v1", user_id="u1")
result = share.revoke()
assert result is None
def test_increment_view_returns_none(self):
share = VideoShare.create(video_id="v1", user_id="u1")
result = share.increment_view_count()
assert result is None
def test_increment_download_returns_none(self):
share = VideoShare.create(video_id="v1", user_id="u1")
result = share.increment_download_count()
assert result is None
def test_id_is_hex(self):
share = VideoShare.create(video_id="v1", user_id="u1")
int(share.id, 16)
def test_ids_are_unique(self):
s1 = VideoShare.create(video_id="v1", user_id="u1")
s2 = VideoShare.create(video_id="v1", user_id="u1")
assert s1.id != s2.id
def test_expires_at_boundary_exact_now(self):
"""expires_at 恰好是现在,应该被认为过期"""
share = VideoShare.create(video_id="v1", user_id="u1")
share.expires_at = datetime.now(timezone.utc) - timedelta(microseconds=1)
assert share.is_expired is True
def test_expires_at_boundary_one_second_future(self):
share = VideoShare.create(video_id="v1", user_id="u1")
share.expires_at = datetime.now(timezone.utc) + timedelta(seconds=1)
assert share.is_expired is False
def test_password_with_special_characters(self):
special_pass = "pass!@#$%^&*()"
share = VideoShare.create(video_id="v1", user_id="u1", password=special_pass)
assert share.verify_password(special_pass) is True
assert share.verify_password("wrong") is False
def test_password_unicode(self):
unicode_pass = "密码🔐测试"
share = VideoShare.create(video_id="v1", user_id="u1", password=unicode_pass)
assert share.verify_password(unicode_pass) is True
+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 == ""