Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a1f42404a0 | |||
| 3a6f765159 |
Executable
+290
@@ -0,0 +1,290 @@
|
||||
"""video_share 视频分享领域实体单测."""
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import pytest
|
||||
from domain.video_share import (
|
||||
VideoShare,
|
||||
_hash_password,
|
||||
generate_share_token,
|
||||
)
|
||||
|
||||
# ── _hash_password ───────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
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) == ""
|
||||
|
||||
def test_same_password_same_hash(self):
|
||||
h1 = _hash_password("mypassword")
|
||||
h2 = _hash_password("mypassword")
|
||||
assert h1 == h2
|
||||
|
||||
def test_different_passwords_different_hashes(self):
|
||||
h1 = _hash_password("password1")
|
||||
h2 = _hash_password("password2")
|
||||
assert h1 != h2
|
||||
|
||||
def test_hash_is_hex_string(self):
|
||||
h = _hash_password("test")
|
||||
assert isinstance(h, str)
|
||||
assert len(h) == 64 # SHA-256 hex
|
||||
int(h, 16) # 应该能被解析为16进制
|
||||
|
||||
def test_hash_contains_salt(self):
|
||||
# 直接的 SHA-256(password) 应该不等于加盐后的
|
||||
from hashlib import sha256
|
||||
|
||||
raw = sha256("mypass".encode()).hexdigest()
|
||||
salted = _hash_password("mypass")
|
||||
assert raw != salted
|
||||
|
||||
|
||||
# ── 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_short_token(self):
|
||||
token = generate_share_token(6)
|
||||
assert len(token) == 6
|
||||
|
||||
def test_url_friendly_chars(self):
|
||||
token = generate_share_token(100)
|
||||
# 不应该有容易混淆的字符 i,l,o,0,1
|
||||
assert "i" not in token
|
||||
assert "l" not in token
|
||||
assert "o" not in token
|
||||
assert "0" not in token
|
||||
assert "1" not in token
|
||||
|
||||
def test_unique_tokens(self):
|
||||
tokens = {generate_share_token() for _ in range(100)}
|
||||
assert len(tokens) == 100 # 应该都是唯一的
|
||||
|
||||
def test_alphanumeric(self):
|
||||
token = generate_share_token(50)
|
||||
assert token.isalnum()
|
||||
|
||||
|
||||
# ── VideoShare.create ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestVideoShareCreate:
|
||||
"""VideoShare.create 工厂方法"""
|
||||
|
||||
def test_minimal_create(self):
|
||||
s = VideoShare.create(video_id="vid_001", user_id="user_001")
|
||||
assert s.id is not None
|
||||
assert len(s.id) == 32 # uuid4 hex
|
||||
assert s.video_id == "vid_001"
|
||||
assert s.user_id == "user_001"
|
||||
assert s.share_token is not None
|
||||
assert len(s.share_token) == 12
|
||||
assert s.password_hash is None
|
||||
assert s.expires_at is None
|
||||
assert s.view_count == 0
|
||||
assert s.download_count == 0
|
||||
assert s.is_active is True
|
||||
|
||||
def test_with_password(self):
|
||||
s = VideoShare.create(video_id="v1", user_id="u1", password="secret123")
|
||||
assert s.password_hash is not None
|
||||
assert s.password_hash != "secret123" # 不是明文
|
||||
assert len(s.password_hash) == 64 # SHA-256
|
||||
|
||||
def test_with_expiry(self):
|
||||
future = datetime.now(timezone.utc) + timedelta(days=7)
|
||||
s = VideoShare.create(video_id="v1", user_id="u1", expires_at=future)
|
||||
assert s.expires_at == future
|
||||
|
||||
def test_empty_video_id_raises(self):
|
||||
with pytest.raises(ValueError, match="video_id"):
|
||||
VideoShare.create(video_id="", user_id="u1")
|
||||
|
||||
def test_whitespace_video_id_raises(self):
|
||||
with pytest.raises(ValueError):
|
||||
VideoShare.create(video_id=" ", user_id="u1")
|
||||
|
||||
def test_empty_user_id_raises(self):
|
||||
with pytest.raises(ValueError, match="user_id"):
|
||||
VideoShare.create(video_id="v1", user_id="")
|
||||
|
||||
def test_past_expiry_raises(self):
|
||||
past = datetime.now(timezone.utc) - timedelta(hours=1)
|
||||
with pytest.raises(ValueError, match="past"):
|
||||
VideoShare.create(video_id="v1", user_id="u1", expires_at=past)
|
||||
|
||||
def test_video_id_stripped(self):
|
||||
s = VideoShare.create(video_id=" vid_123 ", user_id="u1")
|
||||
assert s.video_id == "vid_123"
|
||||
|
||||
def test_user_id_stripped(self):
|
||||
s = VideoShare.create(video_id="v1", user_id=" user_456 ")
|
||||
assert s.user_id == "user_456"
|
||||
|
||||
def test_unique_ids(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_unique_tokens(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
|
||||
|
||||
def test_timestamps_set(self):
|
||||
s = VideoShare.create(video_id="v1", user_id="u1")
|
||||
assert s.created_at.tzinfo is not None
|
||||
assert s.updated_at.tzinfo is not None
|
||||
|
||||
|
||||
# ── VideoShare 属性方法 ─────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestVideoShareProperties:
|
||||
"""VideoShare 属性方法"""
|
||||
|
||||
def test_has_password_true(self):
|
||||
s = VideoShare.create(video_id="v1", user_id="u1", password="pass")
|
||||
assert s.has_password is True
|
||||
|
||||
def test_has_password_false(self):
|
||||
s = VideoShare.create(video_id="v1", user_id="u1")
|
||||
assert s.has_password is False
|
||||
|
||||
def test_is_expired_false_no_expiry(self):
|
||||
s = VideoShare.create(video_id="v1", user_id="u1")
|
||||
assert s.is_expired is False
|
||||
|
||||
def test_is_expired_false_future_expiry(self):
|
||||
future = datetime.now(timezone.utc) + timedelta(hours=1)
|
||||
s = VideoShare.create(video_id="v1", user_id="u1", expires_at=future)
|
||||
assert s.is_expired is False
|
||||
|
||||
def test_is_expired_true_past_expiry(self):
|
||||
# 直接构造一个已过期的
|
||||
past = datetime.now(timezone.utc) - timedelta(hours=1)
|
||||
s = VideoShare(
|
||||
id="test",
|
||||
video_id="v1",
|
||||
user_id="u1",
|
||||
share_token="abc",
|
||||
expires_at=past,
|
||||
)
|
||||
assert s.is_expired is True
|
||||
|
||||
def test_is_accessible_true(self):
|
||||
s = VideoShare.create(video_id="v1", user_id="u1")
|
||||
assert s.is_accessible is True
|
||||
|
||||
def test_is_accessible_false_inactive(self):
|
||||
s = VideoShare.create(video_id="v1", user_id="u1")
|
||||
s.is_active = False
|
||||
assert s.is_accessible is False
|
||||
|
||||
def test_is_accessible_false_expired(self):
|
||||
past = datetime.now(timezone.utc) - timedelta(hours=1)
|
||||
s = VideoShare(
|
||||
id="test",
|
||||
video_id="v1",
|
||||
user_id="u1",
|
||||
share_token="abc",
|
||||
expires_at=past,
|
||||
)
|
||||
assert s.is_accessible is False
|
||||
|
||||
|
||||
# ── VideoShare 方法 ─────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestVideoShareMethods:
|
||||
"""VideoShare 方法"""
|
||||
|
||||
def test_verify_password_no_password_true(self):
|
||||
s = VideoShare.create(video_id="v1", user_id="u1")
|
||||
assert s.verify_password("anything") is True
|
||||
assert s.verify_password("") is True
|
||||
|
||||
def test_verify_password_correct(self):
|
||||
s = VideoShare.create(video_id="v1", user_id="u1", password="mypass")
|
||||
assert s.verify_password("mypass") is True
|
||||
|
||||
def test_verify_password_wrong(self):
|
||||
s = VideoShare.create(video_id="v1", user_id="u1", password="mypass")
|
||||
assert s.verify_password("wrongpass") is False
|
||||
|
||||
def test_verify_password_empty_false(self):
|
||||
s = VideoShare.create(video_id="v1", user_id="u1", password="mypass")
|
||||
assert s.verify_password("") is False
|
||||
|
||||
def test_increment_view_count(self):
|
||||
s = VideoShare.create(video_id="v1", user_id="u1")
|
||||
assert s.view_count == 0
|
||||
s.increment_view_count()
|
||||
assert s.view_count == 1
|
||||
s.increment_view_count()
|
||||
assert s.view_count == 2
|
||||
|
||||
def test_increment_download_count(self):
|
||||
s = VideoShare.create(video_id="v1", user_id="u1")
|
||||
assert s.download_count == 0
|
||||
s.increment_download_count()
|
||||
assert s.download_count == 1
|
||||
s.increment_download_count()
|
||||
assert s.download_count == 2
|
||||
|
||||
def test_revoke(self):
|
||||
s = VideoShare.create(video_id="v1", user_id="u1")
|
||||
assert s.is_active is True
|
||||
s.revoke()
|
||||
assert s.is_active is False
|
||||
|
||||
def test_revoke_makes_inaccessible(self):
|
||||
s = VideoShare.create(video_id="v1", user_id="u1")
|
||||
assert s.is_accessible is True
|
||||
s.revoke()
|
||||
assert s.is_accessible is False
|
||||
|
||||
|
||||
# ── dataclass 基础特性 ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestVideoShareBasics:
|
||||
"""VideoShare 基础特性"""
|
||||
|
||||
def test_slots_no_extra_attrs(self):
|
||||
s = VideoShare.create(video_id="v1", user_id="u1")
|
||||
with pytest.raises(AttributeError):
|
||||
s.nonexistent = "value"
|
||||
|
||||
def test_direct_construction(self):
|
||||
s = VideoShare(
|
||||
id="custom_id",
|
||||
video_id="v1",
|
||||
user_id="u1",
|
||||
share_token="abc123",
|
||||
)
|
||||
assert s.id == "custom_id"
|
||||
assert s.share_token == "abc123"
|
||||
|
||||
def test_equality_same_id(self):
|
||||
now = datetime.now(timezone.utc)
|
||||
s1 = VideoShare(id="same", video_id="v1", user_id="u1", share_token="t", created_at=now, updated_at=now)
|
||||
s2 = VideoShare(id="same", video_id="v1", user_id="u1", share_token="t", created_at=now, updated_at=now)
|
||||
assert s1 == s2
|
||||
Reference in New Issue
Block a user