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 18s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 56s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 48s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 28s
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
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 35s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 29s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 16s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 3m51s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m24s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m13s
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 / Unit Tests (pull_request) Successful in 2m7s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m2s
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
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 48m5s

新增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边界、状态切换等)
This commit is contained in:
CI Bot
2026-07-25 16:17:41 +08:00
parent 96c8a90098
commit ccd8387287
4 changed files with 369 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"
+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