"""视频分享领域模型单元测试 — wave215""" from __future__ import annotations import re from datetime import datetime, timedelta, timezone import pytest from packages.domain.video_share import ( VideoShare, _hash_password, generate_share_token, ) # ── 密码哈希 ───────────────────────────────────────────────────────────────── class TestHashPassword: def test_empty_password_returns_empty(self): assert _hash_password("") == "" def test_same_password_same_hash(self): h1 = _hash_password("secret123") h2 = _hash_password("secret123") assert h1 == h2 assert h1 != "" def test_different_password_different_hash(self): h1 = _hash_password("pass1") h2 = _hash_password("pass2") assert h1 != h2 def test_hash_is_sha256_hex(self): h = _hash_password("test") assert len(h) == 64 assert re.match(r"^[0-9a-f]{64}$", h) def test_hash_contains_salt(self): # 直接SHA-256("test") vs 加盐后的结果应该不同 import hashlib direct = hashlib.sha256(b"test").hexdigest() salted = _hash_password("test") assert direct != salted # ── Token 生成 ────────────────────────────────────────────────────────────── class TestGenerateShareToken: def test_default_length_12(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_no_ambiguous_chars(self): # 不应包含容易混淆的字符:i, l, o, I, L, O, 0, 1 token = generate_share_token(100) for ch in "ilO01": assert ch not in token def test_alphanumeric_only(self): token = generate_share_token(50) assert token.isalnum() def test_two_tokens_different(self): # 随机生成的两个token应该不同 t1 = generate_share_token() t2 = generate_share_token() assert t1 != t2 # ── VideoShare.create ─────────────────────────────────────────────────────── class TestVideoShareCreate: def test_basic_create(self): share = VideoShare.create(video_id="v1", user_id="u1") assert share.id is not None assert share.video_id == "v1" assert share.user_id == "u1" assert share.share_token is not None 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_with_password(self): share = VideoShare.create(video_id="v1", user_id="u1", password="secret") assert share.password_hash is not None assert share.password_hash != "secret" assert len(share.password_hash) == 64 def test_create_with_empty_password_no_hash(self): share = VideoShare.create(video_id="v1", user_id="u1", password="") assert share.password_hash is None 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_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_whitespace_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_strips_whitespace(self): share = VideoShare.create(video_id=" v1 ", user_id=" u1 ") assert share.video_id == "v1" assert share.user_id == "u1" def test_create_unique_id_each_time(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_create_unique_token_each_time(self): s1 = VideoShare.create(video_id="v1", user_id="u1") s2 = VideoShare.create(video_id="v1", user_id="u1") assert s1.share_token != s2.share_token # ── has_password ──────────────────────────────────────────────────────────── class TestVideoShareHasPassword: def test_no_password(self): share = VideoShare.create(video_id="v1", user_id="u1") assert share.has_password is False def test_with_password(self): share = VideoShare.create(video_id="v1", user_id="u1", password="pass") assert share.has_password is True def test_empty_password_none(self): share = VideoShare.create(video_id="v1", user_id="u1", password="") assert share.has_password is False # ── is_expired ────────────────────────────────────────────────────────────── class TestVideoShareIsExpired: def test_no_expiry_never_expired(self): share = VideoShare.create(video_id="v1", user_id="u1") assert share.is_expired is False def test_future_expiry_not_expired(self): future = datetime.now(timezone.utc) + timedelta(hours=1) share = VideoShare.create(video_id="v1", user_id="u1", expires_at=future) assert share.is_expired is False def test_past_expiry_is_expired(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 True # ── is_accessible ─────────────────────────────────────────────────────────── class TestVideoShareIsAccessible: def test_active_no_expiry_accessible(self): share = VideoShare.create(video_id="v1", user_id="u1") assert share.is_accessible is True def test_revoked_not_accessible(self): share = VideoShare.create(video_id="v1", user_id="u1") share.is_active = False assert share.is_accessible is False def test_expired_not_accessible(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_revoked_and_expired_not_accessible(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 # ── verify_password ───────────────────────────────────────────────────────── class TestVideoShareVerifyPassword: def test_no_password_any_pass_ok(self): share = VideoShare.create(video_id="v1", user_id="u1") assert share.verify_password("anything") is True assert share.verify_password("") is True def test_no_password_none_ok(self): share = VideoShare.create(video_id="v1", user_id="u1") assert share.verify_password("") is True def test_correct_password(self): share = VideoShare.create(video_id="v1", user_id="u1", password="mysecret") assert share.verify_password("mysecret") is True def test_wrong_password(self): share = VideoShare.create(video_id="v1", user_id="u1", password="mysecret") assert share.verify_password("wrong") is False def test_empty_password_with_protection(self): share = VideoShare.create(video_id="v1", user_id="u1", password="mysecret") assert share.verify_password("") is False def test_password_case_sensitive(self): share = VideoShare.create(video_id="v1", user_id="u1", password="Secret") assert share.verify_password("secret") is False assert share.verify_password("Secret") is True # ── 计数方法 ──────────────────────────────────────────────────────────────── class TestVideoShareCounters: def test_increment_view(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(self): share = VideoShare.create(video_id="v1", user_id="u1") assert share.download_count == 0 share.increment_download_count() assert share.download_count == 1 share.increment_download_count() assert share.download_count == 2 def test_counters_independent(self): share = VideoShare.create(video_id="v1", user_id="u1") share.increment_view_count() share.increment_view_count() share.increment_download_count() assert share.view_count == 2 assert share.download_count == 1 # ── revoke ────────────────────────────────────────────────────────────────── class TestVideoShareRevoke: def test_revoke_sets_inactive(self): share = VideoShare.create(video_id="v1", user_id="u1") assert share.is_active is True share.revoke() assert share.is_active is False def test_revoke_makes_inaccessible(self): share = VideoShare.create(video_id="v1", user_id="u1") share.revoke() assert share.is_accessible is False def test_revoke_idempotent(self): share = VideoShare.create(video_id="v1", user_id="u1") share.revoke() share.revoke() # 第二次也不报错 assert share.is_active is False