"""GeneratedVideo + VerificationCode 领域模型测试.""" from __future__ import annotations from datetime import timedelta import pytest from packages.domain.generated_video import GeneratedVideo from packages.domain.verification_code import VerificationCode class TestGeneratedVideo: """GeneratedVideo 生成视频实体测试.""" def test_create_success(self): """创建成功.""" video = GeneratedVideo.create( project_id="p1", generation_task_id="task_1", name="我的视频.mp4", file_url="https://example.com/out.mp4", user_id="u1", file_size=1024000, duration=30.5, width=1080, height=1920, fps=25.0, ) assert video.id is not None assert len(video.id) == 32 assert video.project_id == "p1" assert video.generation_task_id == "task_1" assert video.name == "我的视频.mp4" assert video.file_url == "https://example.com/out.mp4" assert video.user_id == "u1" assert video.file_size == 1024000 assert video.duration == pytest.approx(30.5) assert video.width == 1080 assert video.height == 1920 assert video.fps == pytest.approx(25.0) assert video.status == "completed" assert video.review_status == "pending_review" assert video.is_duplicate is False assert video.duplicate_of is None assert video.generation_params == {} def test_create_empty_project_id_raises(self): """空project_id抛异常.""" with pytest.raises(ValueError, match="project_id"): GeneratedVideo.create( project_id=" ", generation_task_id="t1", name="v.mp4", file_url="https://x.com/v.mp4", ) def test_create_empty_task_id_raises(self): """空generation_task_id抛异常.""" with pytest.raises(ValueError, match="generation_task_id"): GeneratedVideo.create( project_id="p1", generation_task_id="", name="v.mp4", file_url="https://x.com/v.mp4", ) def test_create_empty_name_raises(self): """空name抛异常.""" with pytest.raises(ValueError, match="name"): GeneratedVideo.create( project_id="p1", generation_task_id="t1", name=" ", file_url="https://x.com/v.mp4", ) def test_create_empty_file_url_raises(self): """空file_url抛异常.""" with pytest.raises(ValueError, match="file_url"): GeneratedVideo.create( project_id="p1", generation_task_id="t1", name="v.mp4", file_url="", ) def test_create_strips_whitespace(self): """首尾空白被去除.""" video = GeneratedVideo.create( project_id=" p1 ", generation_task_id=" t1 ", name=" 视频.mp4 ", file_url=" https://x.com/v.mp4 ", ) assert video.project_id == "p1" assert video.generation_task_id == "t1" assert video.name == "视频.mp4" assert video.file_url == "https://x.com/v.mp4" def test_default_values(self): """默认值正确.""" video = GeneratedVideo.create( project_id="p1", generation_task_id="t1", name="v.mp4", file_url="https://x.com/v.mp4", ) assert video.user_id == "" assert video.file_size == 0 assert video.duration == 0.0 assert video.width == 0 assert video.height == 0 assert video.fps == 0.0 assert video.thumbnail_url is None assert video.generation_params == {} def test_generation_params_none_becomes_empty(self): """generation_params=None → {}.""" video = GeneratedVideo.create( project_id="p1", generation_task_id="t1", name="v.mp4", file_url="https://x.com/v.mp4", generation_params=None, ) assert video.generation_params == {} def test_custom_generation_params(self): """自定义生成参数.""" params = {"mode": "smart", "resolution": "1080x1920"} video = GeneratedVideo.create( project_id="p1", generation_task_id="t1", name="v.mp4", file_url="https://x.com/v.mp4", generation_params=params, ) assert video.generation_params == params def test_duplicate_flag(self): """重复标记可以设置.""" video = GeneratedVideo.create( project_id="p1", generation_task_id="t1", name="v.mp4", file_url="https://x.com/v.mp4", ) video.is_duplicate = True video.duplicate_of = "other_video_id" assert video.is_duplicate is True assert video.duplicate_of == "other_video_id" def test_custom_status(self): """自定义状态.""" video = GeneratedVideo.create( project_id="p1", generation_task_id="t1", name="v.mp4", file_url="https://x.com/v.mp4", ) video.status = "failed" assert video.status == "failed" def test_thumbnail_url(self): """缩略图URL.""" video = GeneratedVideo.create( project_id="p1", generation_task_id="t1", name="v.mp4", file_url="https://x.com/v.mp4", thumbnail_url="https://x.com/thumb.jpg", ) assert video.thumbnail_url == "https://x.com/thumb.jpg" class TestVerificationCodeCreate: """VerificationCode 创建测试.""" def test_create_success(self): """创建验证码成功.""" vc = VerificationCode.create( recipient="test@example.com", code_type="email_bind", ttl_seconds=300, ) assert vc.id is not None assert len(vc.id) == 32 assert vc.recipient == "test@example.com" assert vc.code_type == "email_bind" assert len(vc.code) == 6 # 默认6位数字 assert vc.code.isdigit() # 纯数字 assert vc.used_at is None assert vc.attempts == 0 def test_create_with_custom_code(self): """自定义验证码.""" vc = VerificationCode.create( recipient="u@test.com", code_type="email_login", custom_code="123456", ) assert vc.code == "123456" def test_create_recipient_stripped(self): """收件人空白被去除.""" vc = VerificationCode.create( recipient=" test@example.com ", code_type="email_bind", ) assert vc.recipient == "test@example.com" def test_expiry_time_correct(self): """过期时间正确(5分钟后).""" from datetime import datetime, timezone before = datetime.now(timezone.utc) + timedelta(seconds=299) vc = VerificationCode.create( recipient="u@test.com", code_type="reset_password", ttl_seconds=300, ) after = datetime.now(timezone.utc) + timedelta(seconds=301) assert before <= vc.expires_at <= after def test_custom_ttl(self): """自定义过期时间.""" vc = VerificationCode.create( recipient="u@test.com", code_type="phone_login", ttl_seconds=60, ) from datetime import datetime, timezone now = datetime.now(timezone.utc) # 应该在1分钟左右过期 diff = (vc.expires_at - now).total_seconds() assert 0 < diff < 70 class TestVerificationCodeProperties: """VerificationCode 属性方法测试.""" def test_is_expired_false_for_new(self): """新创建的验证码未过期.""" vc = VerificationCode.create( recipient="u@test.com", code_type="email_bind", ttl_seconds=300, ) assert vc.is_expired is False def test_is_expired_true_when_past(self): """已过期的验证码is_expired=True.""" vc = VerificationCode.create( recipient="u@test.com", code_type="email_bind", ttl_seconds=1, ) # 手动改过期时间到过去 from datetime import datetime, timedelta, timezone vc.expires_at = datetime.now(timezone.utc) - timedelta(seconds=1) assert vc.is_expired is True def test_is_used_false_by_default(self): """默认未使用.""" vc = VerificationCode.create(recipient="u@test.com", code_type="email_bind") assert vc.is_used is False def test_is_valid_fresh_code(self): """新验证码有效.""" vc = VerificationCode.create(recipient="u@test.com", code_type="email_bind") assert vc.is_valid is True def test_is_valid_expired(self): """过期的验证码无效.""" vc = VerificationCode.create(recipient="u@test.com", code_type="email_bind", ttl_seconds=1) from datetime import datetime, timedelta, timezone vc.expires_at = datetime.now(timezone.utc) - timedelta(seconds=1) assert vc.is_valid is False def test_is_valid_used(self): """已使用的验证码无效.""" vc = VerificationCode.create(recipient="u@test.com", code_type="email_bind") vc.mark_used() assert vc.is_valid is False class TestVerificationCodeActions: """VerificationCode 操作方法测试.""" def test_mark_used(self): """标记使用.""" vc = VerificationCode.create(recipient="u@test.com", code_type="email_bind") vc.mark_used() assert vc.is_used is True assert vc.used_at is not None def test_mark_used_twice(self): """标记两次也没问题.""" vc = VerificationCode.create(recipient="u@test.com", code_type="email_bind") vc.mark_used() first_time = vc.used_at vc.mark_used() # 第二次会覆盖时间 assert vc.used_at >= first_time def test_increment_attempts(self): """增加尝试次数.""" vc = VerificationCode.create(recipient="u@test.com", code_type="email_bind") assert vc.attempts == 0 vc.increment_attempts() assert vc.attempts == 1 vc.increment_attempts() assert vc.attempts == 2 def test_all_code_types_supported(self): """支持所有code_type.""" for code_type in ["email_bind", "phone_bind", "email_login", "phone_login", "reset_password"]: vc = VerificationCode.create(recipient="u@test.com", code_type=code_type) assert vc.code_type == code_type