3921a657e8
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 / Frontend Lint (push) Successful in 52s
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 - Type Check (mypy) (push) Successful in 1m56s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m7s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m7s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 54s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 3m20s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 6m27s
CI/CD Pipeline / Unit Tests (push) Failing after 8m24s
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 16m1s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m14s
CI/CD Pipeline / Integration Tests (push) Successful in 3m28s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 21s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m26s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m34s
515 lines
18 KiB
Python
Executable File
515 lines
18 KiB
Python
Executable File
"""视频分享 UseCase 单元测试."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timedelta, timezone
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
from packages.application.video_share.commands import (
|
|
CreateShareCommand,
|
|
UpdateShareCommand,
|
|
)
|
|
from packages.application.video_share.use_cases import (
|
|
AccessShareUseCase,
|
|
CreateShareUseCase,
|
|
GetShareByTokenUseCase,
|
|
InvalidPasswordError,
|
|
ListSharesByUserUseCase,
|
|
ListSharesByVideoUseCase,
|
|
NotFoundError,
|
|
PasswordRequiredError,
|
|
RecordShareDownloadUseCase,
|
|
RevokeShareUseCase,
|
|
ShareExpiredError,
|
|
UpdateShareUseCase,
|
|
VideoNotFoundError,
|
|
)
|
|
from packages.domain.generated_video import GeneratedVideo
|
|
from packages.domain.video_share import VideoShare
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_share_repo():
|
|
return MagicMock()
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_video_repo():
|
|
return MagicMock()
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_video():
|
|
video = MagicMock(spec=GeneratedVideo)
|
|
video.id = "video_001"
|
|
video.user_id = "user_001"
|
|
return video
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_share():
|
|
share = VideoShare.create(
|
|
video_id="video_001",
|
|
user_id="user_001",
|
|
)
|
|
return share
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_share_with_password():
|
|
share = VideoShare.create(
|
|
video_id="video_001",
|
|
user_id="user_001",
|
|
password="secret123",
|
|
)
|
|
return share
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_share_expired():
|
|
# 直接构造已过期的分享(不经过create方法的校验)
|
|
share = VideoShare(
|
|
id="share_expired_001",
|
|
video_id="video_001",
|
|
user_id="user_001",
|
|
share_token="expiredtoken123",
|
|
expires_at=datetime.now(timezone.utc) - timedelta(hours=1),
|
|
)
|
|
return share
|
|
|
|
|
|
class TestCreateShareUseCase:
|
|
"""CreateShareUseCase 测试"""
|
|
|
|
def test_create_share_success(self, mock_share_repo, mock_video_repo, sample_video):
|
|
"""正常创建分享链接"""
|
|
mock_video_repo.get.return_value = sample_video
|
|
mock_share_repo.create.side_effect = lambda s: s
|
|
|
|
use_case = CreateShareUseCase(mock_share_repo, mock_video_repo)
|
|
command = CreateShareCommand(video_id="video_001", user_id="user_001")
|
|
result = use_case.execute(command)
|
|
|
|
assert result.video_id == "video_001"
|
|
assert result.user_id == "user_001"
|
|
assert result.share_token is not None
|
|
assert result.has_password is False
|
|
mock_share_repo.create.assert_called_once()
|
|
|
|
def test_create_share_with_password(self, mock_share_repo, mock_video_repo, sample_video):
|
|
"""创建带密码的分享"""
|
|
mock_video_repo.get.return_value = sample_video
|
|
mock_share_repo.create.side_effect = lambda s: s
|
|
|
|
use_case = CreateShareUseCase(mock_share_repo, mock_video_repo)
|
|
command = CreateShareCommand(
|
|
video_id="video_001",
|
|
user_id="user_001",
|
|
password="mypassword",
|
|
)
|
|
result = use_case.execute(command)
|
|
|
|
assert result.has_password is True
|
|
assert result.password_hash is not None
|
|
|
|
def test_create_share_with_expiry(self, mock_share_repo, mock_video_repo, sample_video):
|
|
"""创建带有效期的分享"""
|
|
mock_video_repo.get.return_value = sample_video
|
|
mock_share_repo.create.side_effect = lambda s: s
|
|
|
|
future = datetime.now(timezone.utc) + timedelta(days=7)
|
|
use_case = CreateShareUseCase(mock_share_repo, mock_video_repo)
|
|
command = CreateShareCommand(
|
|
video_id="video_001",
|
|
user_id="user_001",
|
|
expires_at=future,
|
|
)
|
|
result = use_case.execute(command)
|
|
|
|
assert result.expires_at == future
|
|
|
|
def test_create_share_video_not_found(self, mock_share_repo, mock_video_repo):
|
|
"""视频不存在时抛出 VideoNotFoundError"""
|
|
mock_video_repo.get.return_value = None
|
|
|
|
use_case = CreateShareUseCase(mock_share_repo, mock_video_repo)
|
|
command = CreateShareCommand(video_id="nonexistent", user_id="user_001")
|
|
|
|
with pytest.raises(VideoNotFoundError):
|
|
use_case.execute(command)
|
|
|
|
mock_share_repo.create.assert_not_called()
|
|
|
|
def test_create_share_wrong_user(self, mock_share_repo, mock_video_repo, sample_video):
|
|
"""非视频所有者创建分享失败"""
|
|
sample_video.user_id = "user_other"
|
|
mock_video_repo.get.return_value = sample_video
|
|
|
|
use_case = CreateShareUseCase(mock_share_repo, mock_video_repo)
|
|
command = CreateShareCommand(video_id="video_001", user_id="user_001")
|
|
|
|
with pytest.raises(VideoNotFoundError):
|
|
use_case.execute(command)
|
|
|
|
mock_share_repo.create.assert_not_called()
|
|
|
|
|
|
class TestGetShareByTokenUseCase:
|
|
"""GetShareByTokenUseCase 测试"""
|
|
|
|
def test_get_share_success(self, mock_share_repo, sample_share):
|
|
"""通过 token 正常获取分享信息"""
|
|
mock_share_repo.get_by_token.return_value = sample_share
|
|
|
|
use_case = GetShareByTokenUseCase(mock_share_repo)
|
|
result = use_case.execute(sample_share.share_token)
|
|
|
|
assert result.id == sample_share.id
|
|
mock_share_repo.get_by_token.assert_called_once_with(sample_share.share_token)
|
|
|
|
def test_get_share_not_found(self, mock_share_repo):
|
|
"""token 不存在时抛出 NotFoundError"""
|
|
mock_share_repo.get_by_token.return_value = None
|
|
|
|
use_case = GetShareByTokenUseCase(mock_share_repo)
|
|
|
|
with pytest.raises(NotFoundError):
|
|
use_case.execute("invalid_token")
|
|
|
|
def test_get_share_expired_raises(self, mock_share_repo, sample_share_expired):
|
|
"""已过期的分享不可访问"""
|
|
mock_share_repo.get_by_token.return_value = sample_share_expired
|
|
|
|
use_case = GetShareByTokenUseCase(mock_share_repo)
|
|
|
|
with pytest.raises(ShareExpiredError):
|
|
use_case.execute(sample_share_expired.share_token)
|
|
|
|
|
|
class TestAccessShareUseCase:
|
|
"""AccessShareUseCase 测试"""
|
|
|
|
def test_access_without_password(self, mock_share_repo, mock_video_repo, sample_share, sample_video):
|
|
"""无密码分享直接访问成功"""
|
|
mock_share_repo.get_by_token.return_value = sample_share
|
|
mock_video_repo.get.return_value = sample_video
|
|
|
|
use_case = AccessShareUseCase(mock_share_repo, mock_video_repo)
|
|
result = use_case.execute(sample_share.share_token)
|
|
|
|
assert result.share.id == sample_share.id
|
|
assert result.video.id == "video_001"
|
|
assert result.password_verified is True
|
|
mock_share_repo.increment_view.assert_called_once_with(sample_share.id)
|
|
assert sample_share.view_count == 1
|
|
|
|
def test_access_with_correct_password(
|
|
self, mock_share_repo, mock_video_repo, sample_share_with_password, sample_video
|
|
):
|
|
"""带密码分享输入正确密码访问成功"""
|
|
mock_share_repo.get_by_token.return_value = sample_share_with_password
|
|
mock_video_repo.get.return_value = sample_video
|
|
|
|
use_case = AccessShareUseCase(mock_share_repo, mock_video_repo)
|
|
result = use_case.execute(sample_share_with_password.share_token, password="secret123")
|
|
|
|
assert result.password_verified is True
|
|
mock_share_repo.increment_view.assert_called_once()
|
|
|
|
def test_access_password_required_but_not_provided(
|
|
self, mock_share_repo, mock_video_repo, sample_share_with_password
|
|
):
|
|
"""带密码分享不输入密码抛出 PasswordRequiredError"""
|
|
mock_share_repo.get_by_token.return_value = sample_share_with_password
|
|
|
|
use_case = AccessShareUseCase(mock_share_repo, mock_video_repo)
|
|
|
|
with pytest.raises(PasswordRequiredError):
|
|
use_case.execute(sample_share_with_password.share_token)
|
|
|
|
mock_share_repo.increment_view.assert_not_called()
|
|
|
|
def test_access_wrong_password(self, mock_share_repo, mock_video_repo, sample_share_with_password):
|
|
"""密码错误抛出 InvalidPasswordError"""
|
|
mock_share_repo.get_by_token.return_value = sample_share_with_password
|
|
|
|
use_case = AccessShareUseCase(mock_share_repo, mock_video_repo)
|
|
|
|
with pytest.raises(InvalidPasswordError):
|
|
use_case.execute(sample_share_with_password.share_token, password="wrongpass")
|
|
|
|
mock_share_repo.increment_view.assert_not_called()
|
|
|
|
def test_access_share_not_found(self, mock_share_repo, mock_video_repo):
|
|
"""分享不存在抛出 NotFoundError"""
|
|
mock_share_repo.get_by_token.return_value = None
|
|
|
|
use_case = AccessShareUseCase(mock_share_repo, mock_video_repo)
|
|
|
|
with pytest.raises(NotFoundError):
|
|
use_case.execute("invalid_token")
|
|
|
|
def test_access_expired_share(self, mock_share_repo, mock_video_repo, sample_share_expired):
|
|
"""已过期分享不可访问"""
|
|
mock_share_repo.get_by_token.return_value = sample_share_expired
|
|
|
|
use_case = AccessShareUseCase(mock_share_repo, mock_video_repo)
|
|
|
|
with pytest.raises(ShareExpiredError):
|
|
use_case.execute(sample_share_expired.share_token)
|
|
|
|
mock_share_repo.increment_view.assert_not_called()
|
|
|
|
def test_access_video_not_found(self, mock_share_repo, mock_video_repo, sample_share):
|
|
"""分享存在但视频不存在"""
|
|
mock_share_repo.get_by_token.return_value = sample_share
|
|
mock_video_repo.get.return_value = None
|
|
|
|
use_case = AccessShareUseCase(mock_share_repo, mock_video_repo)
|
|
|
|
with pytest.raises(VideoNotFoundError):
|
|
use_case.execute(sample_share.share_token)
|
|
|
|
|
|
class TestListSharesByVideoUseCase:
|
|
"""ListSharesByVideoUseCase 测试"""
|
|
|
|
def test_list_by_video(self, mock_share_repo, sample_share):
|
|
"""列出某个视频的所有分享"""
|
|
mock_share_repo.list_by_video.return_value = [sample_share]
|
|
|
|
use_case = ListSharesByVideoUseCase(mock_share_repo)
|
|
result = use_case.execute("video_001", "user_001")
|
|
|
|
assert len(result) == 1
|
|
mock_share_repo.list_by_video.assert_called_once_with("video_001", "user_001")
|
|
|
|
def test_list_by_video_empty(self, mock_share_repo):
|
|
"""视频没有分享记录时返回空列表"""
|
|
mock_share_repo.list_by_video.return_value = []
|
|
|
|
use_case = ListSharesByVideoUseCase(mock_share_repo)
|
|
result = use_case.execute("video_001", "user_001")
|
|
|
|
assert result == []
|
|
|
|
|
|
class TestListSharesByUserUseCase:
|
|
"""ListSharesByUserUseCase 测试"""
|
|
|
|
def test_list_by_user(self, mock_share_repo, sample_share):
|
|
"""列出用户的所有分享"""
|
|
mock_share_repo.list_by_user.return_value = [sample_share]
|
|
mock_share_repo.count_by_user.return_value = 1
|
|
|
|
use_case = ListSharesByUserUseCase(mock_share_repo)
|
|
items, total = use_case.execute("user_001")
|
|
|
|
assert len(items) == 1
|
|
assert total == 1
|
|
mock_share_repo.list_by_user.assert_called_once_with("user_001", skip=0, limit=20)
|
|
|
|
def test_list_by_user_with_pagination(self, mock_share_repo):
|
|
"""带分页参数查询"""
|
|
mock_share_repo.list_by_user.return_value = []
|
|
mock_share_repo.count_by_user.return_value = 50
|
|
|
|
use_case = ListSharesByUserUseCase(mock_share_repo)
|
|
items, total = use_case.execute("user_001", skip=10, limit=5)
|
|
|
|
assert total == 50
|
|
mock_share_repo.list_by_user.assert_called_once_with("user_001", skip=10, limit=5)
|
|
|
|
def test_list_by_user_empty(self, mock_share_repo):
|
|
"""用户没有分享记录"""
|
|
mock_share_repo.list_by_user.return_value = []
|
|
mock_share_repo.count_by_user.return_value = 0
|
|
|
|
use_case = ListSharesByUserUseCase(mock_share_repo)
|
|
items, total = use_case.execute("user_001")
|
|
|
|
assert items == []
|
|
assert total == 0
|
|
|
|
|
|
class TestUpdateShareUseCase:
|
|
"""UpdateShareUseCase 测试"""
|
|
|
|
def test_update_password(self, mock_share_repo, sample_share):
|
|
"""更新分享密码"""
|
|
mock_share_repo.get_by_id.return_value = sample_share
|
|
mock_share_repo.update.side_effect = lambda s: s
|
|
|
|
use_case = UpdateShareUseCase(mock_share_repo)
|
|
command = UpdateShareCommand(
|
|
share_id=sample_share.id,
|
|
user_id="user_001",
|
|
password="newpassword",
|
|
)
|
|
result = use_case.execute(command)
|
|
|
|
assert result.has_password is True
|
|
mock_share_repo.update.assert_called_once()
|
|
|
|
def test_clear_password(self, mock_share_repo, sample_share_with_password):
|
|
"""清除分享密码(空字符串)"""
|
|
mock_share_repo.get_by_id.return_value = sample_share_with_password
|
|
mock_share_repo.update.side_effect = lambda s: s
|
|
|
|
use_case = UpdateShareUseCase(mock_share_repo)
|
|
command = UpdateShareCommand(
|
|
share_id=sample_share_with_password.id,
|
|
user_id="user_001",
|
|
password="", # 空字符串表示清除
|
|
)
|
|
result = use_case.execute(command)
|
|
|
|
assert result.has_password is False
|
|
assert result.password_hash is None
|
|
|
|
def test_update_password_none_no_change(self, mock_share_repo, sample_share_with_password):
|
|
"""password=None 不修改密码"""
|
|
original_hash = sample_share_with_password.password_hash
|
|
mock_share_repo.get_by_id.return_value = sample_share_with_password
|
|
mock_share_repo.update.side_effect = lambda s: s
|
|
|
|
use_case = UpdateShareUseCase(mock_share_repo)
|
|
command = UpdateShareCommand(
|
|
share_id=sample_share_with_password.id,
|
|
user_id="user_001",
|
|
password=None, # None表示不修改
|
|
)
|
|
result = use_case.execute(command)
|
|
|
|
assert result.password_hash == original_hash
|
|
|
|
def test_update_expires_at(self, mock_share_repo, sample_share):
|
|
"""更新有效期"""
|
|
mock_share_repo.get_by_id.return_value = sample_share
|
|
mock_share_repo.update.side_effect = lambda s: s
|
|
|
|
future = datetime.now(timezone.utc) + timedelta(days=3)
|
|
use_case = UpdateShareUseCase(mock_share_repo)
|
|
command = UpdateShareCommand(
|
|
share_id=sample_share.id,
|
|
user_id="user_001",
|
|
expires_at=future,
|
|
)
|
|
result = use_case.execute(command)
|
|
|
|
assert result.expires_at == future
|
|
|
|
def test_update_expires_at_past_raises(self, mock_share_repo, sample_share):
|
|
"""设置过去的有效期抛出 ValueError"""
|
|
mock_share_repo.get_by_id.return_value = sample_share
|
|
|
|
past = datetime.now(timezone.utc) - timedelta(hours=1)
|
|
use_case = UpdateShareUseCase(mock_share_repo)
|
|
command = UpdateShareCommand(
|
|
share_id=sample_share.id,
|
|
user_id="user_001",
|
|
expires_at=past,
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="expires_at cannot be in the past"):
|
|
use_case.execute(command)
|
|
|
|
mock_share_repo.update.assert_not_called()
|
|
|
|
def test_update_share_not_found(self, mock_share_repo):
|
|
"""分享不存在抛出 NotFoundError"""
|
|
mock_share_repo.get_by_id.return_value = None
|
|
|
|
use_case = UpdateShareUseCase(mock_share_repo)
|
|
command = UpdateShareCommand(
|
|
share_id="nonexistent",
|
|
user_id="user_001",
|
|
password="newpass",
|
|
)
|
|
|
|
with pytest.raises(NotFoundError):
|
|
use_case.execute(command)
|
|
|
|
mock_share_repo.update.assert_not_called()
|
|
|
|
|
|
class TestRevokeShareUseCase:
|
|
"""RevokeShareUseCase 测试"""
|
|
|
|
def test_revoke_success(self, mock_share_repo, sample_share):
|
|
"""撤销分享成功"""
|
|
mock_share_repo.get_by_id.return_value = sample_share
|
|
mock_share_repo.delete.return_value = True
|
|
|
|
use_case = RevokeShareUseCase(mock_share_repo)
|
|
result = use_case.execute(sample_share.id, "user_001")
|
|
|
|
assert result is True
|
|
mock_share_repo.delete.assert_called_once_with(sample_share.id, "user_001")
|
|
|
|
def test_revoke_not_found(self, mock_share_repo):
|
|
"""分享不存在抛出 NotFoundError"""
|
|
mock_share_repo.get_by_id.return_value = None
|
|
|
|
use_case = RevokeShareUseCase(mock_share_repo)
|
|
|
|
with pytest.raises(NotFoundError):
|
|
use_case.execute("nonexistent", "user_001")
|
|
|
|
mock_share_repo.delete.assert_not_called()
|
|
|
|
|
|
class TestRecordShareDownloadUseCase:
|
|
"""RecordShareDownloadUseCase 测试"""
|
|
|
|
def test_record_download_no_password(self, mock_share_repo, sample_share):
|
|
"""无密码分享记录下载"""
|
|
mock_share_repo.get_by_token.return_value = sample_share
|
|
|
|
use_case = RecordShareDownloadUseCase(mock_share_repo)
|
|
use_case.execute(sample_share.share_token)
|
|
|
|
mock_share_repo.increment_download.assert_called_once_with(sample_share.id)
|
|
|
|
def test_record_download_with_password(self, mock_share_repo, sample_share_with_password):
|
|
"""带密码分享正确密码记录下载"""
|
|
mock_share_repo.get_by_token.return_value = sample_share_with_password
|
|
|
|
use_case = RecordShareDownloadUseCase(mock_share_repo)
|
|
use_case.execute(sample_share_with_password.share_token, password="secret123")
|
|
|
|
mock_share_repo.increment_download.assert_called_once()
|
|
|
|
def test_record_download_wrong_password(self, mock_share_repo, sample_share_with_password):
|
|
"""密码错误不记录下载"""
|
|
mock_share_repo.get_by_token.return_value = sample_share_with_password
|
|
|
|
use_case = RecordShareDownloadUseCase(mock_share_repo)
|
|
|
|
with pytest.raises(InvalidPasswordError):
|
|
use_case.execute(sample_share_with_password.share_token, password="wrong")
|
|
|
|
mock_share_repo.increment_download.assert_not_called()
|
|
|
|
def test_record_download_not_found(self, mock_share_repo):
|
|
"""分享不存在抛出 NotFoundError"""
|
|
mock_share_repo.get_by_token.return_value = None
|
|
|
|
use_case = RecordShareDownloadUseCase(mock_share_repo)
|
|
|
|
with pytest.raises(NotFoundError):
|
|
use_case.execute("invalid_token")
|
|
|
|
def test_record_download_expired(self, mock_share_repo, sample_share_expired):
|
|
"""已过期分享不能下载"""
|
|
mock_share_repo.get_by_token.return_value = sample_share_expired
|
|
|
|
use_case = RecordShareDownloadUseCase(mock_share_repo)
|
|
|
|
with pytest.raises(ShareExpiredError):
|
|
use_case.execute(sample_share_expired.share_token)
|
|
|
|
mock_share_repo.increment_download.assert_not_called()
|