dfbd141895
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 - Type Check (mypy) (push) Successful in 1m54s
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 / Validate - Migration (alembic) (push) Successful in 2m1s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m14s
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Successful in 2m22s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m8s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 3m33s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m25s
CI/CD Pipeline / Integration Tests (push) Successful in 2m38s
CI/CD Pipeline / Unit Tests (push) Failing after 6m7s
CI/CD Pipeline / Build Staging API Image (push) Successful in 12m16s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 3m12s
CI/CD Pipeline / ACR Image Cleanup (push) Failing after 13s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 5m30s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 6m24s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
323 lines
11 KiB
Python
Executable File
323 lines
11 KiB
Python
Executable File
"""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
|
||
|
||
|
||
class TestGenerateShareTokenExtended:
|
||
"""generate_share_token 深度补充测试"""
|
||
|
||
def test_zero_length(self):
|
||
token = generate_share_token(0)
|
||
assert token == ""
|
||
|
||
def test_length_one(self):
|
||
token = generate_share_token(1)
|
||
assert len(token) == 1
|
||
|
||
def test_very_long_token(self):
|
||
token = generate_share_token(100)
|
||
assert len(token) == 100
|
||
|
||
def test_no_special_characters(self):
|
||
token = generate_share_token(50)
|
||
assert token.isalnum()
|
||
|
||
def test_all_characters_from_alphabet(self):
|
||
alphabet = set("abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ23456789")
|
||
token = generate_share_token(200)
|
||
for c in token:
|
||
assert c in alphabet
|
||
|
||
|
||
class TestVideoShareExtended:
|
||
"""VideoShare 深度补充测试"""
|
||
|
||
def test_zero_view_count(self):
|
||
share = VideoShare.create(video_id="v1", user_id="u1")
|
||
assert share.view_count == 0
|
||
|
||
def test_zero_download_count(self):
|
||
share = VideoShare.create(video_id="v1", user_id="u1")
|
||
assert share.download_count == 0
|
||
|
||
def test_large_view_count(self):
|
||
share = VideoShare.create(video_id="v1", user_id="u1")
|
||
for _ in range(1000):
|
||
share.increment_view_count()
|
||
assert share.view_count == 1000
|
||
|
||
def test_revoke_idempotent(self):
|
||
share = VideoShare.create(video_id="v1", user_id="u1")
|
||
share.revoke()
|
||
assert share.is_active is False
|
||
share.revoke()
|
||
assert share.is_active is False
|
||
|
||
def test_revoke_returns_none(self):
|
||
share = VideoShare.create(video_id="v1", user_id="u1")
|
||
result = share.revoke()
|
||
assert result is None
|
||
|
||
def test_increment_view_returns_none(self):
|
||
share = VideoShare.create(video_id="v1", user_id="u1")
|
||
result = share.increment_view_count()
|
||
assert result is None
|
||
|
||
def test_increment_download_returns_none(self):
|
||
share = VideoShare.create(video_id="v1", user_id="u1")
|
||
result = share.increment_download_count()
|
||
assert result is None
|
||
|
||
def test_id_is_hex(self):
|
||
share = VideoShare.create(video_id="v1", user_id="u1")
|
||
int(share.id, 16)
|
||
|
||
def test_ids_are_unique(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_expires_at_boundary_exact_now(self):
|
||
"""expires_at 恰好是现在,应该被认为过期"""
|
||
share = VideoShare.create(video_id="v1", user_id="u1")
|
||
share.expires_at = datetime.now(timezone.utc) - timedelta(microseconds=1)
|
||
assert share.is_expired is True
|
||
|
||
def test_expires_at_boundary_one_second_future(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 False
|
||
|
||
def test_password_with_special_characters(self):
|
||
special_pass = "pass!@#$%^&*()"
|
||
share = VideoShare.create(video_id="v1", user_id="u1", password=special_pass)
|
||
assert share.verify_password(special_pass) is True
|
||
assert share.verify_password("wrong") is False
|
||
|
||
def test_password_unicode(self):
|
||
unicode_pass = "密码🔐测试"
|
||
share = VideoShare.create(video_id="v1", user_id="u1", password=unicode_pass)
|
||
assert share.verify_password(unicode_pass) is True
|