Compare commits

...

2 Commits

Author SHA1 Message Date
CI Bot a1f42404a0 style: auto-format with black + isort + prettier [skip ci-format-check]
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 45s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m15s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m45s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m49s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m54s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m37s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 2m37s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m44s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 5m12s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 4m37s
AI Code Review / AI Code Review (pull_request) Successful in 6m51s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m51s
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 9s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
ACR Cleanup / ACR Image Cleanup (pull_request_target) Has been cancelled
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 48s
2026-07-29 13:48:14 +00:00
xiaoxia 3a6f765159 test(domain): add wave193 video_share unit tests (+43)
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 38s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m19s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m32s
AI Code Review / AI Code Review (pull_request) Successful in 1m31s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m37s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m41s
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
Add comprehensive unit tests for VideoShare domain entity:
- Basic construction, slots, equality
- create() factory with validation (video_id/user_id, expiry)
- Property checks: has_password, is_expired, is_accessible
- Methods: verify_password, increment_view/download_count, revoke
- generate_share_token helper and password hashing
2026-07-29 21:45:15 +08:00
+290
View File
@@ -0,0 +1,290 @@
"""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