Files
xiaoxia-saas/tests/unit/test_video_share_domain.py
xiaoxia 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
test(wave83): generated_video/verification/video_share单测 +55 (#891)
2026-07-25 22:33:26 +08:00

323 lines
11 KiB
Python
Executable File
Raw Permalink 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
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