Files
xiaoxia-saas/tests/unit/test_video_share_domain.py
T
CI Bot 2df7bc9dc8
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m41s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m45s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m54s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 3m1s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m34s
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 5m49s
CI/CD Pipeline / Frontend Lint (push) Successful in 6m0s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m27s
CI/CD Pipeline / Integration Tests (push) Successful in 2m20s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m11s
CI/CD Pipeline / Unit Tests (push) Failing after 5m52s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 30s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m48s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 5m10s
style: auto-format with black + isort + prettier
2026-07-24 13:35:05 +00:00

226 lines
8.1 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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