"""VideoShare 领域层单元测试 - video_share.py""" from datetime import datetime, timedelta, timezone import pytest from packages.domain.video_share import ( VideoShare, _hash_password, generate_share_token, ) class TestGenerateShareToken: """generate_share_token 函数测试""" def test_default_length(self): token = generate_share_token() assert len(token) == 12 def test_custom_length(self): token = generate_share_token(20) assert len(token) == 20 def test_url_friendly_characters(self): """token 只包含 URL 友好的字符(没有 l, i, o, 0, 1 等易混字符)""" token = generate_share_token(100) # 不应包含易混淆字符 assert "l" not in token assert "i" not in token assert "o" not in token assert "0" not in token assert "1" not in token def test_randomness(self): """两次生成的 token 不同(概率上)""" tokens = {generate_share_token() for _ in range(100)} # 100 次应该几乎不可能重复 assert len(tokens) > 95 class TestHashPassword: """_hash_password 函数测试""" def test_empty_password_returns_empty(self): assert _hash_password("") == "" def test_none_password_returns_empty(self): assert _hash_password(None) == "" # type: ignore def test_hash_is_deterministic(self): """相同密码哈希结果相同""" h1 = _hash_password("mypassword") h2 = _hash_password("mypassword") assert h1 == h2 def test_hash_differs_for_different_passwords(self): """不同密码哈希结果不同""" h1 = _hash_password("password1") h2 = _hash_password("password2") assert h1 != h2 def test_hash_is_hex_string(self): """哈希是 64 位十六进制字符串(SHA-256)""" h = _hash_password("test") assert len(h) == 64 int(h, 16) # 应该能解析为十六进制 def test_hash_includes_salt(self): """加盐后与直接 SHA-256 不同""" from hashlib import sha256 direct = sha256("mypass".encode()).hexdigest() salted = _hash_password("mypass") assert direct != salted class TestVideoShareCreate: """VideoShare.create 工厂方法测试""" def test_create_basic(self): share = VideoShare.create(video_id="video-1", user_id="user-1") assert share.id assert len(share.id) == 32 assert share.video_id == "video-1" assert share.user_id == "user-1" assert share.share_token assert len(share.share_token) == 12 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 assert share.created_at is not None assert share.updated_at is not None def test_create_empty_video_id_raises(self): with pytest.raises(ValueError, match="video_id cannot be empty"): VideoShare.create(video_id=" ", user_id="u1") def test_create_empty_user_id_raises(self): with pytest.raises(ValueError, match="user_id cannot be empty"): VideoShare.create(video_id="v1", user_id=" ") def test_create_with_password(self): share = VideoShare.create(video_id="v1", user_id="u1", password="secret123") assert share.password_hash is not None assert share.password_hash != "secret123" # 已哈希 assert len(share.password_hash) == 64 # SHA-256 hex def test_create_with_expires_at(self): future = datetime.now(timezone.utc) + timedelta(days=7) share = VideoShare.create(video_id="v1", user_id="u1", expires_at=future) assert share.expires_at == future def test_create_past_expires_at_raises(self): past = datetime.now(timezone.utc) - timedelta(days=1) with pytest.raises(ValueError, match="expires_at cannot be in the past"): VideoShare.create(video_id="v1", user_id="u1", expires_at=past) def test_create_fields_stripped(self): share = VideoShare.create(video_id=" v1 ", user_id=" u1 ") assert share.video_id == "v1" assert share.user_id == "u1" def test_unique_tokens(self): """不同分享有不同的 token""" shares = [VideoShare.create(video_id="v1", user_id="u1") for _ in range(20)] tokens = [s.share_token for s in shares] assert len(set(tokens)) == 20 class TestVideoShareProperties: """属性测试""" def test_has_password_true(self): share = VideoShare.create(video_id="v1", user_id="u1", password="secret") assert share.has_password is True def test_has_password_false(self): share = VideoShare.create(video_id="v1", user_id="u1") assert share.has_password is False def test_is_expired_no_expiry(self): share = VideoShare.create(video_id="v1", user_id="u1") assert share.is_expired is False def test_is_expired_future_expiry(self): future = datetime.now(timezone.utc) + timedelta(days=7) share = VideoShare.create(video_id="v1", user_id="u1", expires_at=future) assert share.is_expired is False def test_is_expired_past_expiry(self): # 直接设置 expires_at 为过去时间(绕过 create 的校验) share = VideoShare.create(video_id="v1", user_id="u1") share.expires_at = datetime.now(timezone.utc) - timedelta(days=1) assert share.is_expired is True def test_is_accessible_active_not_expired(self): share = VideoShare.create(video_id="v1", user_id="u1") assert share.is_accessible is True def test_is_accessible_inactive(self): share = VideoShare.create(video_id="v1", user_id="u1") share.is_active = False assert share.is_accessible is False def test_is_accessible_expired(self): share = VideoShare.create(video_id="v1", user_id="u1") share.expires_at = datetime.now(timezone.utc) - timedelta(days=1) assert share.is_accessible is False def test_is_accessible_inactive_and_expired(self): share = VideoShare.create(video_id="v1", user_id="u1") share.is_active = False share.expires_at = datetime.now(timezone.utc) - timedelta(days=1) assert share.is_accessible is False class TestVideoSharePassword: """密码验证测试""" def test_verify_password_correct(self): share = VideoShare.create(video_id="v1", user_id="u1", password="secret123") assert share.verify_password("secret123") is True def test_verify_password_wrong(self): share = VideoShare.create(video_id="v1", user_id="u1", password="secret123") assert share.verify_password("wrongpass") is False def test_verify_no_password_always_true(self): """没有设置密码时,任何密码都通过(包括空密码)""" share = VideoShare.create(video_id="v1", user_id="u1") assert share.verify_password("") is True assert share.verify_password("anything") is True def test_verify_empty_password_with_password_set(self): """有密码时,空密码不通过""" share = VideoShare.create(video_id="v1", user_id="u1", password="secret") assert share.verify_password("") is False class TestVideoShareCounters: """计数方法测试""" def test_increment_view_count(self): share = VideoShare.create(video_id="v1", user_id="u1") assert share.view_count == 0 share.increment_view_count() assert share.view_count == 1 share.increment_view_count() assert share.view_count == 2 def test_increment_download_count(self): share = VideoShare.create(video_id="v1", user_id="u1") assert share.download_count == 0 share.increment_download_count() assert share.download_count == 1 def test_revoke(self): share = VideoShare.create(video_id="v1", user_id="u1") assert share.is_active is True 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