"""视频分享领域模型单元测试 - 纯逻辑部分。""" from __future__ import annotations from datetime import datetime, timedelta, timezone import pytest from domain.video_share import ( VideoShare, _hash_password, generate_share_token, ) class TestHashPassword: """密码哈希函数。""" def test_empty_password_returns_empty(self): assert _hash_password("") == "" def test_none_password_returns_empty(self): assert _hash_password(None) == "" def test_same_password_same_hash(self): h1 = _hash_password("test123") h2 = _hash_password("test123") assert h1 == h2 assert len(h1) > 0 def test_different_passwords_different_hash(self): h1 = _hash_password("password1") h2 = _hash_password("password2") assert h1 != h2 def test_hash_is_hex_string(self): h = _hash_password("test") int(h, 16) # 合法 hex 不抛异常 assert len(h) == 64 # SHA-256 输出 64 个 hex 字符 def test_hash_contains_salt(self): """相同密码的直接 SHA-256 与加盐后结果不同。""" from hashlib import sha256 password = "mypassword" direct_hash = sha256(password.encode()).hexdigest() salted_hash = _hash_password(password) assert salted_hash != direct_hash class TestGenerateShareToken: """分享 token 生成。""" def test_default_length(self): token = generate_share_token() assert len(token) == 12 def test_custom_length(self): for length in [6, 8, 16, 32]: token = generate_share_token(length=length) assert len(token) == length def test_url_friendly_alphabet(self): """token 只包含 URL 友好的字符,没有歧义字符。""" token = generate_share_token(length=100) # 不应该包含容易混淆的字符 assert "i" not in token or True # 可能有,取决于随机 assert "l" not in token or True # 验证所有字符都在字母表里 alphabet = "abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ23456789" for char in token: assert char in alphabet def test_tokens_are_unique(self): """连续生成的 token 不重复。""" tokens = {generate_share_token() for _ in range(100)} assert len(tokens) == 100 class TestVideoShareCreate: """VideoShare.create 工厂方法。""" def test_create_minimal(self): share = VideoShare.create(video_id="vid-1", user_id="user-1") assert share.video_id == "vid-1" assert share.user_id == "user-1" assert share.id # 自动生成 assert share.share_token # 自动生成 assert share.password_hash is None assert share.expires_at is None assert share.view_count == 0 assert share.download_count == 0 assert share.is_active is True def test_create_with_password(self): share = VideoShare.create(video_id="vid-1", user_id="user-1", password="secret123") assert share.password_hash is not None assert share.password_hash != "secret123" # 已哈希 assert len(share.password_hash) > 0 def test_create_with_expiration(self): expire_time = datetime(2026, 12, 31, tzinfo=timezone.utc) share = VideoShare.create(video_id="vid-1", user_id="user-1", expires_at=expire_time) assert share.expires_at == expire_time def test_create_generates_unique_ids(self): s1 = VideoShare.create(video_id="v", user_id="u") s2 = VideoShare.create(video_id="v", user_id="u") assert s1.id != s2.id assert s1.share_token != s2.share_token def test_create_strips_whitespace(self): share = VideoShare.create(video_id=" vid-1 ", user_id="\tuser-1\n") assert share.video_id == "vid-1" assert share.user_id == "user-1" class TestVideoSharePassword: """密码相关方法。""" def test_has_password_false_when_none(self): share = VideoShare.create(video_id="v", user_id="u") assert share.has_password is False def test_has_password_false_when_empty(self): share = VideoShare.create(video_id="v", user_id="u", password="") assert share.has_password is False def test_has_password_true_when_set(self): share = VideoShare.create(video_id="v", user_id="u", password="pass") assert share.has_password is True def test_verify_password_correct(self): share = VideoShare.create(video_id="v", user_id="u", password="mysecret") assert share.verify_password("mysecret") is True def test_verify_password_wrong(self): share = VideoShare.create(video_id="v", user_id="u", password="mysecret") assert share.verify_password("wrong") is False def test_verify_password_no_password_set(self): share = VideoShare.create(video_id="v", user_id="u") # 没有设置密码时,任何输入都通过(免密访问) assert share.verify_password("anything") is True assert share.verify_password("") is True def test_verify_password_empty_input(self): share = VideoShare.create(video_id="v", user_id="u", password="pass") assert share.verify_password("") is False class TestVideoShareExpiration: """过期相关方法。""" def test_not_expired_when_no_expiry(self): share = VideoShare.create(video_id="v", user_id="u") assert share.is_expired is False def test_not_expired_when_future(self): future = datetime.now(timezone.utc) + timedelta(days=7) share = VideoShare.create(video_id="v", user_id="u", expires_at=future) assert share.is_expired is False def test_expired_when_past(self): share = VideoShare.create(video_id="v", user_id="u") # 直接设置过去的过期时间(create 方法会阻止过期时间在过去) share.expires_at = datetime.now(timezone.utc) - timedelta(days=1) assert share.is_expired is True def test_create_rejects_past_expiry(self): """create 方法拒绝过去的过期时间。""" past = datetime.now(timezone.utc) - timedelta(days=1) with pytest.raises(ValueError, match="expires_at cannot be in the past"): VideoShare.create(video_id="v", user_id="u", expires_at=past) class TestVideoShareAccessible: """可访问性判断。""" def test_active_no_expiry_is_accessible(self): share = VideoShare.create(video_id="v", user_id="u") assert share.is_accessible is True def test_inactive_not_accessible(self): share = VideoShare.create(video_id="v", user_id="u") share.revoke() assert share.is_accessible is False def test_expired_not_accessible(self): share = VideoShare.create(video_id="v", user_id="u") # 直接设置过去的过期时间 share.expires_at = datetime.now(timezone.utc) - timedelta(days=1) assert share.is_accessible is False class TestVideoShareCounts: """计数相关方法。""" def test_initial_view_count_zero(self): share = VideoShare.create(video_id="v", user_id="u") assert share.view_count == 0 def test_increment_view_count(self): share = VideoShare.create(video_id="v", user_id="u") share.increment_view_count() assert share.view_count == 1 share.increment_view_count() share.increment_view_count() assert share.view_count == 3 def test_initial_download_count_zero(self): share = VideoShare.create(video_id="v", user_id="u") assert share.download_count == 0 def test_increment_download_count(self): share = VideoShare.create(video_id="v", user_id="u") share.increment_download_count() share.increment_download_count() assert share.download_count == 2 class TestVideoShareRevoke: """撤销分享。""" def test_revoke_deactivates(self): share = VideoShare.create(video_id="v", user_id="u") assert share.is_active is True share.revoke() assert share.is_active is False def test_revoke_idempotent(self): share = VideoShare.create(video_id="v", user_id="u") share.revoke() share.revoke() # 再次调用不报错 assert share.is_active is False