e902fbd65e
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (push) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / PR Build API Image (push) Has been cancelled
CI/CD Pipeline / PR Build Web Image (push) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
291 lines
10 KiB
Python
Executable File
291 lines
10 KiB
Python
Executable File
"""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
|