9eca947f88
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (push) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / PR Build API Image (push) Has been cancelled
CI/CD Pipeline / PR Build Web Image (push) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
432 lines
14 KiB
Python
432 lines
14 KiB
Python
"""小领域模块组合单测 — template_clip_config / tag / exceptions / editing_mode."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from packages.domain.editing_mode import EditingMode
|
|
from packages.domain.exceptions import (
|
|
DomainError,
|
|
NotFoundError,
|
|
QuotaExceededError,
|
|
ValidationError,
|
|
)
|
|
from packages.domain.tag import Tag
|
|
from packages.domain.template_clip_config import (
|
|
ClipType,
|
|
TemplateClipConfig,
|
|
TransitionEffect,
|
|
)
|
|
|
|
# ── EditingMode ────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestEditingMode:
|
|
def test_one_take(self):
|
|
assert EditingMode.ONE_TAKE == "one_take"
|
|
|
|
def test_pip(self):
|
|
assert EditingMode.PIP == "pip"
|
|
|
|
def test_voice_over(self):
|
|
assert EditingMode.VOICE_OVER == "voice_over"
|
|
|
|
def test_voice_pip(self):
|
|
assert EditingMode.VOICE_PIP == "voice_pip"
|
|
|
|
def test_from_string(self):
|
|
assert EditingMode("one_take") == EditingMode.ONE_TAKE
|
|
assert EditingMode("pip") == EditingMode.PIP
|
|
|
|
def test_invalid_raises(self):
|
|
with pytest.raises(ValueError):
|
|
EditingMode("invalid_mode")
|
|
|
|
def test_is_str(self):
|
|
# StrEnum 实例本身就是 str
|
|
assert isinstance(EditingMode.ONE_TAKE, str)
|
|
assert EditingMode.ONE_TAKE + "_suffix" == "one_take_suffix"
|
|
|
|
|
|
# ── 异常类 ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestExceptions:
|
|
def test_domain_error_is_exception(self):
|
|
err = DomainError("test")
|
|
assert isinstance(err, Exception)
|
|
assert str(err) == "test"
|
|
|
|
def test_not_found_inherits_domain(self):
|
|
err = NotFoundError("not found")
|
|
assert isinstance(err, DomainError)
|
|
assert str(err) == "not found"
|
|
|
|
def test_validation_error_inherits_domain(self):
|
|
err = ValidationError("bad input")
|
|
assert isinstance(err, DomainError)
|
|
assert str(err) == "bad input"
|
|
|
|
def test_quota_exceeded_error_attributes(self):
|
|
err = QuotaExceededError(dimension="storage_gb", limit=100, used=150)
|
|
assert err.dimension == "storage_gb"
|
|
assert err.limit == 100
|
|
assert err.used == 150
|
|
assert "storage_gb" in str(err)
|
|
assert "150" in str(err)
|
|
assert "100" in str(err)
|
|
|
|
def test_quota_exceeded_is_domain_error(self):
|
|
err = QuotaExceededError("x", 10, 20)
|
|
assert isinstance(err, DomainError)
|
|
|
|
def test_catch_domain_error_catches_all(self):
|
|
"""所有领域异常都能被 DomainError catch."""
|
|
for cls in [NotFoundError, ValidationError, QuotaExceededError]:
|
|
try:
|
|
if cls == QuotaExceededError:
|
|
raise cls("dim", 10, 20)
|
|
raise cls("msg")
|
|
except DomainError:
|
|
pass
|
|
else:
|
|
pytest.fail(f"{cls.__name__} 未被 DomainError 捕获")
|
|
|
|
|
|
# ── Tag ────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestTag:
|
|
def test_create_basic(self):
|
|
tag = Tag.create(user_id="user1", name="旅行")
|
|
assert tag.id
|
|
assert tag.user_id == "user1"
|
|
assert tag.name == "旅行"
|
|
|
|
def test_create_name_stripped(self):
|
|
tag = Tag.create(user_id="u1", name=" 美食 ")
|
|
assert tag.name == "美食"
|
|
|
|
def test_create_empty_name_raises(self):
|
|
with pytest.raises(ValueError, match="标签名称不能为空"):
|
|
Tag.create(user_id="u1", name="")
|
|
|
|
def test_create_whitespace_name_raises(self):
|
|
with pytest.raises(ValueError, match="标签名称不能为空"):
|
|
Tag.create(user_id="u1", name=" ")
|
|
|
|
def test_create_has_created_at(self):
|
|
tag = Tag.create(user_id="u1", name="t1")
|
|
assert tag.created_at is not None
|
|
|
|
def test_unique_ids(self):
|
|
t1 = Tag.create(user_id="u1", name="t1")
|
|
t2 = Tag.create(user_id="u1", name="t2")
|
|
assert t1.id != t2.id
|
|
|
|
|
|
# ── ClipType 枚举 ─────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestClipType:
|
|
def test_all_types(self):
|
|
assert ClipType.INTRO == "intro"
|
|
assert ClipType.MAIN == "main"
|
|
assert ClipType.TRANSITION == "transition"
|
|
assert ClipType.OUTRO == "outro"
|
|
assert ClipType.TITLE == "title"
|
|
assert ClipType.SUBTITLE == "subtitle"
|
|
|
|
def test_from_string(self):
|
|
assert ClipType("main") == ClipType.MAIN
|
|
assert ClipType("intro") == ClipType.INTRO
|
|
|
|
def test_invalid_raises(self):
|
|
with pytest.raises(ValueError):
|
|
ClipType("invalid")
|
|
|
|
|
|
# ── TransitionEffect 枚举 ─────────────────────────────────────────────────
|
|
|
|
|
|
class TestTransitionEffect:
|
|
def test_all_effects(self):
|
|
assert TransitionEffect.CUT == "cut"
|
|
assert TransitionEffect.FADE == "fade"
|
|
assert TransitionEffect.SLIDE_LEFT == "slide_left"
|
|
assert TransitionEffect.SLIDE_RIGHT == "slide_right"
|
|
assert TransitionEffect.DISSOLVE == "dissolve"
|
|
assert TransitionEffect.WIPE == "wipe"
|
|
|
|
def test_from_string(self):
|
|
assert TransitionEffect("fade") == TransitionEffect.FADE
|
|
assert TransitionEffect("cut") == TransitionEffect.CUT
|
|
|
|
def test_invalid_raises(self):
|
|
with pytest.raises(ValueError):
|
|
TransitionEffect("invalid_effect")
|
|
|
|
|
|
# ── TemplateClipConfig.create ──────────────────────────────────────────────
|
|
|
|
|
|
class TestTemplateClipConfigCreate:
|
|
def test_basic_creation(self):
|
|
clip = TemplateClipConfig.create(
|
|
template_id="tmpl_1",
|
|
clip_type=ClipType.MAIN,
|
|
order=1,
|
|
)
|
|
assert clip.id
|
|
assert clip.template_id == "tmpl_1"
|
|
assert clip.clip_type == ClipType.MAIN
|
|
assert clip.order == 1
|
|
assert clip.min_duration == 0.0
|
|
assert clip.max_duration == 0.0
|
|
assert clip.text_template == ""
|
|
assert clip.material_requirements == {}
|
|
assert clip.transition_effect == TransitionEffect.CUT
|
|
assert clip.config == {}
|
|
|
|
def test_clip_type_string(self):
|
|
clip = TemplateClipConfig.create(
|
|
template_id="t1",
|
|
clip_type="intro",
|
|
order=0,
|
|
)
|
|
assert clip.clip_type == ClipType.INTRO
|
|
|
|
def test_template_id_stripped(self):
|
|
clip = TemplateClipConfig.create(
|
|
template_id=" tmpl_1 ",
|
|
clip_type=ClipType.MAIN,
|
|
order=1,
|
|
)
|
|
assert clip.template_id == "tmpl_1"
|
|
|
|
def test_empty_template_id_raises(self):
|
|
with pytest.raises(ValueError, match="template_id 不能为空"):
|
|
TemplateClipConfig.create(template_id="", clip_type=ClipType.MAIN, order=1)
|
|
|
|
def test_whitespace_template_id_raises(self):
|
|
with pytest.raises(ValueError, match="template_id 不能为空"):
|
|
TemplateClipConfig.create(template_id=" ", clip_type=ClipType.MAIN, order=1)
|
|
|
|
def test_negative_min_duration_raises(self):
|
|
with pytest.raises(ValueError, match="min_duration 不能为负数"):
|
|
TemplateClipConfig.create(
|
|
template_id="t1",
|
|
clip_type=ClipType.MAIN,
|
|
order=1,
|
|
min_duration=-1.0,
|
|
)
|
|
|
|
def test_negative_max_duration_raises(self):
|
|
with pytest.raises(ValueError, match="max_duration 不能为负数"):
|
|
TemplateClipConfig.create(
|
|
template_id="t1",
|
|
clip_type=ClipType.MAIN,
|
|
order=1,
|
|
max_duration=-1.0,
|
|
)
|
|
|
|
def test_min_greater_than_max_raises(self):
|
|
with pytest.raises(ValueError, match="min_duration 不能大于 max_duration"):
|
|
TemplateClipConfig.create(
|
|
template_id="t1",
|
|
clip_type=ClipType.MAIN,
|
|
order=1,
|
|
min_duration=10.0,
|
|
max_duration=5.0,
|
|
)
|
|
|
|
def test_zero_min_and_max_ok(self):
|
|
clip = TemplateClipConfig.create(
|
|
template_id="t1",
|
|
clip_type=ClipType.MAIN,
|
|
order=1,
|
|
min_duration=0,
|
|
max_duration=0,
|
|
)
|
|
assert clip.min_duration == 0
|
|
assert clip.max_duration == 0
|
|
|
|
def test_min_zero_max_positive_ok(self):
|
|
clip = TemplateClipConfig.create(
|
|
template_id="t1",
|
|
clip_type=ClipType.MAIN,
|
|
order=1,
|
|
min_duration=0,
|
|
max_duration=10.0,
|
|
)
|
|
assert clip.max_duration == 10.0
|
|
|
|
def test_min_equals_max_ok(self):
|
|
clip = TemplateClipConfig.create(
|
|
template_id="t1",
|
|
clip_type=ClipType.MAIN,
|
|
order=1,
|
|
min_duration=5.0,
|
|
max_duration=5.0,
|
|
)
|
|
assert clip.min_duration == 5.0
|
|
assert clip.max_duration == 5.0
|
|
|
|
def test_with_text_template(self):
|
|
clip = TemplateClipConfig.create(
|
|
template_id="t1",
|
|
clip_type=ClipType.MAIN,
|
|
order=1,
|
|
text_template=" 欢迎收看 {channel} ",
|
|
)
|
|
# text_template 会 strip
|
|
assert clip.text_template == "欢迎收看 {channel}"
|
|
|
|
def test_with_material_requirements(self):
|
|
reqs = {"material_type": "video", "min_duration": 3}
|
|
clip = TemplateClipConfig.create(
|
|
template_id="t1",
|
|
clip_type=ClipType.MAIN,
|
|
order=1,
|
|
material_requirements=reqs,
|
|
)
|
|
assert clip.material_requirements == reqs
|
|
|
|
def test_material_requirements_none_defaults_empty(self):
|
|
clip = TemplateClipConfig.create(
|
|
template_id="t1",
|
|
clip_type=ClipType.MAIN,
|
|
order=1,
|
|
material_requirements=None,
|
|
)
|
|
assert clip.material_requirements == {}
|
|
|
|
def test_transition_effect_string(self):
|
|
clip = TemplateClipConfig.create(
|
|
template_id="t1",
|
|
clip_type=ClipType.MAIN,
|
|
order=1,
|
|
transition_effect="fade",
|
|
)
|
|
assert clip.transition_effect == TransitionEffect.FADE
|
|
|
|
def test_with_config(self):
|
|
config = {"speed": 1.5, "filter": "vibrance"}
|
|
clip = TemplateClipConfig.create(
|
|
template_id="t1",
|
|
clip_type=ClipType.MAIN,
|
|
order=1,
|
|
config=config,
|
|
)
|
|
assert clip.config == config
|
|
|
|
def test_config_none_defaults_empty(self):
|
|
clip = TemplateClipConfig.create(
|
|
template_id="t1",
|
|
clip_type=ClipType.MAIN,
|
|
order=1,
|
|
config=None,
|
|
)
|
|
assert clip.config == {}
|
|
|
|
def test_created_at_and_updated_at(self):
|
|
clip = TemplateClipConfig.create(
|
|
template_id="t1",
|
|
clip_type=ClipType.MAIN,
|
|
order=1,
|
|
)
|
|
assert clip.created_at is not None
|
|
assert clip.updated_at is not None
|
|
|
|
def test_unique_ids(self):
|
|
c1 = TemplateClipConfig.create(template_id="t1", clip_type=ClipType.MAIN, order=1)
|
|
c2 = TemplateClipConfig.create(template_id="t1", clip_type=ClipType.MAIN, order=2)
|
|
assert c1.id != c2.id
|
|
|
|
|
|
# ── TemplateClipConfig 属性 ────────────────────────────────────────────────
|
|
|
|
|
|
class TestTemplateClipConfigProperties:
|
|
def test_has_duration_range_false_both_zero(self):
|
|
clip = TemplateClipConfig.create(
|
|
template_id="t1",
|
|
clip_type=ClipType.MAIN,
|
|
order=1,
|
|
)
|
|
assert clip.has_duration_range is False
|
|
|
|
def test_has_duration_range_true_min_only(self):
|
|
clip = TemplateClipConfig.create(
|
|
template_id="t1",
|
|
clip_type=ClipType.MAIN,
|
|
order=1,
|
|
min_duration=2.0,
|
|
)
|
|
assert clip.has_duration_range is True
|
|
|
|
def test_has_duration_range_true_max_only(self):
|
|
clip = TemplateClipConfig.create(
|
|
template_id="t1",
|
|
clip_type=ClipType.MAIN,
|
|
order=1,
|
|
max_duration=10.0,
|
|
)
|
|
assert clip.has_duration_range is True
|
|
|
|
def test_has_duration_range_true_both_set(self):
|
|
clip = TemplateClipConfig.create(
|
|
template_id="t1",
|
|
clip_type=ClipType.MAIN,
|
|
order=1,
|
|
min_duration=2.0,
|
|
max_duration=10.0,
|
|
)
|
|
assert clip.has_duration_range is True
|
|
|
|
def test_default_duration_zero(self):
|
|
clip = TemplateClipConfig.create(
|
|
template_id="t1",
|
|
clip_type=ClipType.MAIN,
|
|
order=1,
|
|
)
|
|
assert clip.default_duration == 0.0
|
|
|
|
def test_default_duration_min_only(self):
|
|
clip = TemplateClipConfig.create(
|
|
template_id="t1",
|
|
clip_type=ClipType.MAIN,
|
|
order=1,
|
|
min_duration=3.0,
|
|
)
|
|
assert clip.default_duration == 3.0
|
|
|
|
def test_default_duration_max_only(self):
|
|
clip = TemplateClipConfig.create(
|
|
template_id="t1",
|
|
clip_type=ClipType.MAIN,
|
|
order=1,
|
|
max_duration=10.0,
|
|
)
|
|
assert clip.default_duration == 10.0
|
|
|
|
def test_default_duration_both_midpoint(self):
|
|
clip = TemplateClipConfig.create(
|
|
template_id="t1",
|
|
clip_type=ClipType.MAIN,
|
|
order=1,
|
|
min_duration=2.0,
|
|
max_duration=8.0,
|
|
)
|
|
assert clip.default_duration == 5.0
|
|
|
|
def test_default_duration_min_equals_max(self):
|
|
clip = TemplateClipConfig.create(
|
|
template_id="t1",
|
|
clip_type=ClipType.MAIN,
|
|
order=1,
|
|
min_duration=5.0,
|
|
max_duration=5.0,
|
|
)
|
|
assert clip.default_duration == 5.0
|