4251970b49
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
235 lines
8.2 KiB
Python
Executable File
235 lines
8.2 KiB
Python
Executable File
"""视频分享领域模型单元测试 - 纯逻辑部分。"""
|
|
|
|
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
|