"""视频分享 Use Cases 单元测试 — wave215""" 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, ShareAccessResult, ShareExpiredError, UpdateShareUseCase, VideoNotFoundError, ) from packages.domain.generated_video import GeneratedVideo from packages.domain.video_share import VideoShare # ── helpers ────────────────────────────────────────────────────────────────── def _make_share( video_id="v1", user_id="u1", password=None, expires_at=None, is_active=True, view_count=0, download_count=0, ): share = VideoShare.create( video_id=video_id, user_id=user_id, password=password, expires_at=expires_at, ) share.is_active = is_active share.view_count = view_count share.download_count = download_count return share def _make_video(video_id="v1", user_id="u1", name="test.mp4", file_url="http://x/v.mp4"): return GeneratedVideo( id=video_id, project_id="p1", generation_task_id="t1", name=name, file_url=file_url, file_size=1024, duration=10.0, width=1920, height=1080, fps=30.0, user_id=user_id, ) # ── CreateShareUseCase ────────────────────────────────────────────────────── class TestCreateShareUseCase: def test_create_success(self): video = _make_video() share_repo = MagicMock() video_repo = MagicMock() video_repo.get.return_value = video share_repo.create.side_effect = lambda s: s uc = CreateShareUseCase(share_repo, video_repo) cmd = CreateShareCommand(video_id="v1", user_id="u1") result = uc.execute(cmd) assert result.video_id == "v1" assert result.user_id == "u1" video_repo.get.assert_called_once_with("v1") share_repo.create.assert_called_once() def test_create_with_password(self): video = _make_video() share_repo = MagicMock() video_repo = MagicMock() video_repo.get.return_value = video share_repo.create.side_effect = lambda s: s uc = CreateShareUseCase(share_repo, video_repo) cmd = CreateShareCommand(video_id="v1", user_id="u1", password="secret") result = uc.execute(cmd) assert result.has_password is True def test_video_not_found_raises(self): share_repo = MagicMock() video_repo = MagicMock() video_repo.get.return_value = None uc = CreateShareUseCase(share_repo, video_repo) cmd = CreateShareCommand(video_id="v999", user_id="u1") with pytest.raises(VideoNotFoundError): uc.execute(cmd) def test_wrong_user_video_not_found(self): video = _make_video(user_id="u2") share_repo = MagicMock() video_repo = MagicMock() video_repo.get.return_value = video uc = CreateShareUseCase(share_repo, video_repo) cmd = CreateShareCommand(video_id="v1", user_id="u1") with pytest.raises(VideoNotFoundError): uc.execute(cmd) def test_video_without_user_id_attribute(self): # 视频没有user_id字段的情况 class SimpleVideo: pass video = SimpleVideo() video.id = "v1" share_repo = MagicMock() video_repo = MagicMock() video_repo.get.return_value = video share_repo.create.side_effect = lambda s: s uc = CreateShareUseCase(share_repo, video_repo) cmd = CreateShareCommand(video_id="v1", user_id="u1") result = uc.execute(cmd) assert result is not None # ── GetShareByTokenUseCase ────────────────────────────────────────────────── class TestGetShareByTokenUseCase: def test_get_success(self): share = _make_share() repo = MagicMock() repo.get_by_token.return_value = share uc = GetShareByTokenUseCase(repo) result = uc.execute(share.share_token) assert result.id == share.id def test_not_found_raises(self): repo = MagicMock() repo.get_by_token.return_value = None uc = GetShareByTokenUseCase(repo) with pytest.raises(NotFoundError): uc.execute("nonexistent") def test_expired_share_raises(self): share = _make_share() share.expires_at = datetime.now(timezone.utc) - timedelta(days=1) repo = MagicMock() repo.get_by_token.return_value = share uc = GetShareByTokenUseCase(repo) with pytest.raises(ShareExpiredError): uc.execute(share.share_token) def test_revoked_share_raises(self): share = _make_share(is_active=False) repo = MagicMock() repo.get_by_token.return_value = share uc = GetShareByTokenUseCase(repo) with pytest.raises(ShareExpiredError): uc.execute(share.share_token) # ── AccessShareUseCase ────────────────────────────────────────────────────── class TestAccessShareUseCase: def test_access_no_password(self): share = _make_share() video = _make_video() share_repo = MagicMock() video_repo = MagicMock() share_repo.get_by_token.return_value = share video_repo.get.return_value = video uc = AccessShareUseCase(share_repo, video_repo) result = uc.execute(share.share_token) assert isinstance(result, ShareAccessResult) assert result.share.id == share.id assert result.video.id == video.id assert result.password_verified is True assert share.view_count == 1 share_repo.increment_view.assert_called_once_with(share.id) def test_access_with_correct_password(self): share = _make_share(password="secret") video = _make_video() share_repo = MagicMock() video_repo = MagicMock() share_repo.get_by_token.return_value = share video_repo.get.return_value = video uc = AccessShareUseCase(share_repo, video_repo) result = uc.execute(share.share_token, password="secret") assert result.password_verified is True def test_access_password_required_but_not_provided(self): share = _make_share(password="secret") share_repo = MagicMock() video_repo = MagicMock() share_repo.get_by_token.return_value = share uc = AccessShareUseCase(share_repo, video_repo) with pytest.raises(PasswordRequiredError): uc.execute(share.share_token) def test_access_wrong_password(self): share = _make_share(password="secret") share_repo = MagicMock() video_repo = MagicMock() share_repo.get_by_token.return_value = share uc = AccessShareUseCase(share_repo, video_repo) with pytest.raises(InvalidPasswordError): uc.execute(share.share_token, password="wrong") def test_access_expired_share(self): share = _make_share() share.expires_at = datetime.now(timezone.utc) - timedelta(days=1) share_repo = MagicMock() share_repo.get_by_token.return_value = share uc = AccessShareUseCase(share_repo, MagicMock()) with pytest.raises(ShareExpiredError): uc.execute(share.share_token) def test_access_share_not_found(self): share_repo = MagicMock() share_repo.get_by_token.return_value = None uc = AccessShareUseCase(share_repo, MagicMock()) with pytest.raises(NotFoundError): uc.execute("nonexistent") def test_access_video_not_found(self): share = _make_share() share_repo = MagicMock() video_repo = MagicMock() share_repo.get_by_token.return_value = share video_repo.get.return_value = None uc = AccessShareUseCase(share_repo, video_repo) with pytest.raises(VideoNotFoundError): uc.execute(share.share_token) # ── ListSharesByVideoUseCase ──────────────────────────────────────────────── class TestListSharesByVideoUseCase: def test_list_success(self): shares = [_make_share(), _make_share()] repo = MagicMock() repo.list_by_video.return_value = shares uc = ListSharesByVideoUseCase(repo) result = uc.execute("v1", "u1") assert len(result) == 2 repo.list_by_video.assert_called_once_with("v1", "u1") def test_list_empty(self): repo = MagicMock() repo.list_by_video.return_value = [] uc = ListSharesByVideoUseCase(repo) result = uc.execute("v1", "u1") assert result == [] # ── ListSharesByUserUseCase ───────────────────────────────────────────────── class TestListSharesByUserUseCase: def test_list_with_pagination(self): shares = [_make_share() for _ in range(5)] repo = MagicMock() repo.list_by_user.return_value = shares repo.count_by_user.return_value = 20 uc = ListSharesByUserUseCase(repo) items, total = uc.execute("u1", skip=0, limit=5) assert len(items) == 5 assert total == 20 repo.list_by_user.assert_called_once_with("u1", skip=0, limit=5) repo.count_by_user.assert_called_once_with("u1") def test_list_default_params(self): repo = MagicMock() repo.list_by_user.return_value = [] repo.count_by_user.return_value = 0 uc = ListSharesByUserUseCase(repo) uc.execute("u1") repo.list_by_user.assert_called_once_with("u1", skip=0, limit=20) # ── UpdateShareUseCase ────────────────────────────────────────────────────── class TestUpdateShareUseCase: def test_update_password(self): share = _make_share(password="oldpass") repo = MagicMock() repo.get_by_id.return_value = share repo.update.side_effect = lambda s: s uc = UpdateShareUseCase(repo) cmd = UpdateShareCommand(share_id=share.id, user_id="u1", password="newpass") result = uc.execute(cmd) assert result is not None assert share.verify_password("newpass") is True assert share.verify_password("oldpass") is False repo.update.assert_called_once() def test_clear_password(self): share = _make_share(password="oldpass") repo = MagicMock() repo.get_by_id.return_value = share repo.update.side_effect = lambda s: s uc = UpdateShareUseCase(repo) cmd = UpdateShareCommand(share_id=share.id, user_id="u1", password="") result = uc.execute(cmd) assert result.has_password is False def test_update_password_none_no_change(self): share = _make_share(password="oldpass") repo = MagicMock() repo.get_by_id.return_value = share repo.update.side_effect = lambda s: s uc = UpdateShareUseCase(repo) cmd = UpdateShareCommand(share_id=share.id, user_id="u1", password=None) result = uc.execute(cmd) # password=None 表示不修改 assert result.has_password is True assert share.verify_password("oldpass") is True def test_update_expires_at(self): share = _make_share() new_expiry = datetime.now(timezone.utc) + timedelta(days=30) repo = MagicMock() repo.get_by_id.return_value = share repo.update.side_effect = lambda s: s uc = UpdateShareUseCase(repo) cmd = UpdateShareCommand(share_id=share.id, user_id="u1", expires_at=new_expiry) result = uc.execute(cmd) assert result.expires_at == new_expiry def test_update_expires_at_past_raises(self): share = _make_share() past = datetime.now(timezone.utc) - timedelta(days=1) repo = MagicMock() repo.get_by_id.return_value = share uc = UpdateShareUseCase(repo) cmd = UpdateShareCommand(share_id=share.id, user_id="u1", expires_at=past) with pytest.raises(ValueError, match="expires_at cannot be in the past"): uc.execute(cmd) def test_update_not_found_raises(self): repo = MagicMock() repo.get_by_id.return_value = None uc = UpdateShareUseCase(repo) cmd = UpdateShareCommand(share_id="nonexistent", user_id="u1") with pytest.raises(NotFoundError): uc.execute(cmd) def test_update_wrong_user_not_found(self): share = _make_share(user_id="u2") repo = MagicMock() repo.get_by_id.return_value = None # 仓储层已经按user_id过滤了 uc = UpdateShareUseCase(repo) cmd = UpdateShareCommand(share_id=share.id, user_id="u1") with pytest.raises(NotFoundError): uc.execute(cmd) # ── RevokeShareUseCase ────────────────────────────────────────────────────── class TestRevokeShareUseCase: def test_revoke_success(self): repo = MagicMock() repo.get_by_id.return_value = MagicMock() repo.delete.return_value = True uc = RevokeShareUseCase(repo) result = uc.execute("s1", "u1") assert result is True repo.delete.assert_called_once_with("s1", "u1") def test_revoke_not_found_raises(self): repo = MagicMock() repo.get_by_id.return_value = None uc = RevokeShareUseCase(repo) with pytest.raises(NotFoundError): uc.execute("s1", "u1") # ── RecordShareDownloadUseCase ────────────────────────────────────────────── class TestRecordShareDownloadUseCase: def test_record_download_success(self): share = _make_share(download_count=3) repo = MagicMock() repo.get_by_token.return_value = share uc = RecordShareDownloadUseCase(repo) uc.execute(share.share_token) repo.increment_download.assert_called_once_with(share.id) def test_record_download_with_password(self): share = _make_share(password="secret") repo = MagicMock() repo.get_by_token.return_value = share uc = RecordShareDownloadUseCase(repo) uc.execute(share.share_token, password="secret") repo.increment_download.assert_called_once() def test_record_download_wrong_password_raises(self): share = _make_share(password="secret") repo = MagicMock() repo.get_by_token.return_value = share uc = RecordShareDownloadUseCase(repo) with pytest.raises(InvalidPasswordError): uc.execute(share.share_token, password="wrong") def test_record_download_expired_raises(self): share = _make_share() share.expires_at = datetime.now(timezone.utc) - timedelta(days=1) repo = MagicMock() repo.get_by_token.return_value = share uc = RecordShareDownloadUseCase(repo) with pytest.raises(ShareExpiredError): uc.execute(share.share_token) def test_record_download_not_found_raises(self): repo = MagicMock() repo.get_by_token.return_value = None uc = RecordShareDownloadUseCase(repo) with pytest.raises(NotFoundError): uc.execute("nonexistent")