Files
xiaoxia-saas/tests/unit/test_projects_use_cases.py
T
xiaoxia 2ee110f19c
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (push) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / PR Build API Image (push) Has been cancelled
CI/CD Pipeline / PR Build Web Image (push) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
test: P3-1 第33波单元测试(projects/generated_videos/duplication) (#810)
2026-07-24 11:25:49 +08:00

299 lines
10 KiB
Python
Executable File

"""项目 UseCase 单元测试."""
from __future__ import annotations
from unittest.mock import MagicMock
import pytest
from packages.application.projects import (
CreateProjectCommand,
CreateProjectUseCase,
DeleteProjectUseCase,
GetProjectUseCase,
ListProjectsUseCase,
ShareProjectUseCase,
UnshareProjectUseCase,
)
from packages.domain import Project
@pytest.fixture
def mock_repo():
return MagicMock()
@pytest.fixture
def sample_project():
p = Project.create(owner_user_id="user_1", name="测试项目", description="测试描述")
p.id = "proj_123"
return p
class TestListProjectsUseCase:
"""ListProjectsUseCase 测试"""
def test_list_returns_repo_results(self, mock_repo, sample_project):
"""正常返回 repository 查询结果"""
mock_repo.find_accessible_projects.return_value = [sample_project]
use_case = ListProjectsUseCase(mock_repo)
result = use_case.execute("user_1")
assert len(result) == 1
assert result[0].id == "proj_123"
mock_repo.find_accessible_projects.assert_called_once_with("user_1")
def test_empty_user_id_raises(self, mock_repo):
"""空 user_id 抛出 ValueError"""
use_case = ListProjectsUseCase(mock_repo)
with pytest.raises(ValueError, match="user_id 不能为空"):
use_case.execute("")
mock_repo.find_accessible_projects.assert_not_called()
def test_whitespace_user_id_raises(self, mock_repo):
"""纯空格 user_id 也抛出"""
use_case = ListProjectsUseCase(mock_repo)
with pytest.raises(ValueError, match="user_id 不能为空"):
use_case.execute(" ")
def test_user_id_stripped(self, mock_repo, sample_project):
"""user_id 会被 strip 后查询"""
mock_repo.find_accessible_projects.return_value = [sample_project]
use_case = ListProjectsUseCase(mock_repo)
use_case.execute(" user_1 ")
mock_repo.find_accessible_projects.assert_called_once_with("user_1")
class TestGetProjectUseCase:
"""GetProjectUseCase 测试"""
def test_get_existing_project(self, mock_repo, sample_project):
"""获取存在的项目"""
mock_repo.find_by_id.return_value = sample_project
use_case = GetProjectUseCase(mock_repo)
result = use_case.execute("proj_123")
assert result is not None
assert result.id == "proj_123"
mock_repo.find_by_id.assert_called_once_with("proj_123")
def test_get_nonexistent_returns_none(self, mock_repo):
"""获取不存在的项目返回 None"""
mock_repo.find_by_id.return_value = None
use_case = GetProjectUseCase(mock_repo)
result = use_case.execute("nonexistent")
assert result is None
def test_empty_project_id_raises(self, mock_repo):
"""空 project_id 抛出"""
use_case = GetProjectUseCase(mock_repo)
with pytest.raises(ValueError, match="project_id 不能为空"):
use_case.execute("")
def test_project_id_stripped(self, mock_repo, sample_project):
"""project_id 会被 strip"""
mock_repo.find_by_id.return_value = sample_project
use_case = GetProjectUseCase(mock_repo)
use_case.execute(" proj_123 ")
mock_repo.find_by_id.assert_called_once_with("proj_123")
class TestCreateProjectUseCase:
"""CreateProjectUseCase 测试"""
def test_create_success(self, mock_repo, sample_project):
"""创建成功返回 Project"""
mock_repo.save.return_value = sample_project
use_case = CreateProjectUseCase(mock_repo)
command = CreateProjectCommand(name="新项目", description="新描述")
result = use_case.execute(command, "user_1")
assert result.id == "proj_123"
mock_repo.save.assert_called_once()
saved = mock_repo.save.call_args[0][0]
assert isinstance(saved, Project)
assert saved.owner_user_id == "user_1"
assert saved.name == "新项目"
assert saved.description == "新描述"
def test_create_without_description(self, mock_repo):
"""不传 description 使用默认值"""
mock_repo.save.side_effect = lambda x: x
use_case = CreateProjectUseCase(mock_repo)
command = CreateProjectCommand(name="极简项目")
result = use_case.execute(command, "user_1")
assert result.name == "极简项目"
assert result.description == ""
def test_create_empty_name_raises(self, mock_repo):
"""空项目名在 domain 层抛出"""
use_case = CreateProjectUseCase(mock_repo)
command = CreateProjectCommand(name="")
with pytest.raises(ValueError, match="项目名称不能为空"):
use_case.execute(command, "user_1")
mock_repo.save.assert_not_called()
class TestShareProjectUseCase:
"""ShareProjectUseCase 测试"""
def test_share_success(self, mock_repo, sample_project):
"""所有者成功共享项目"""
mock_repo.find_by_id.return_value = sample_project
mock_repo.save.side_effect = lambda x: x
use_case = ShareProjectUseCase(mock_repo)
result = use_case.execute("proj_123", "user_1", "user_2")
assert "user_2" in result.shared_users
mock_repo.save.assert_called_once()
def test_share_nonexistent_project_raises(self, mock_repo):
"""项目不存在时抛出"""
mock_repo.find_by_id.return_value = None
use_case = ShareProjectUseCase(mock_repo)
with pytest.raises(ValueError, match="项目不存在"):
use_case.execute("noexist", "user_1", "user_2")
def test_share_not_owner_raises(self, mock_repo, sample_project):
"""非所有者不能共享"""
mock_repo.find_by_id.return_value = sample_project
use_case = ShareProjectUseCase(mock_repo)
with pytest.raises(ValueError, match="只有项目所有者可以共享"):
use_case.execute("proj_123", "user_other", "user_2")
mock_repo.save.assert_not_called()
def test_share_already_shared_no_duplicate(self, mock_repo, sample_project):
"""已共享的用户不会重复添加"""
sample_project.shared_users = ["user_2"]
mock_repo.find_by_id.return_value = sample_project
mock_repo.save.side_effect = lambda x: x
use_case = ShareProjectUseCase(mock_repo)
result = use_case.execute("proj_123", "user_1", "user_2")
assert result.shared_users.count("user_2") == 1
mock_repo.save.assert_not_called()
class TestUnshareProjectUseCase:
"""UnshareProjectUseCase 测试"""
def test_unshare_success(self, mock_repo, sample_project):
"""所有者成功取消共享"""
sample_project.shared_users = ["user_2", "user_3"]
mock_repo.find_by_id.return_value = sample_project
mock_repo.save.side_effect = lambda x: x
use_case = UnshareProjectUseCase(mock_repo)
result = use_case.execute("proj_123", "user_1", "user_2")
assert "user_2" not in result.shared_users
assert "user_3" in result.shared_users
mock_repo.save.assert_called_once()
def test_unshare_nonexistent_project_raises(self, mock_repo):
"""项目不存在时抛出"""
mock_repo.find_by_id.return_value = None
use_case = UnshareProjectUseCase(mock_repo)
with pytest.raises(ValueError, match="项目不存在"):
use_case.execute("noexist", "user_1", "user_2")
def test_unshare_not_owner_raises(self, mock_repo, sample_project):
"""非所有者不能取消共享"""
sample_project.shared_users = ["user_2"]
mock_repo.find_by_id.return_value = sample_project
use_case = UnshareProjectUseCase(mock_repo)
with pytest.raises(ValueError, match="只有项目所有者可以取消共享"):
use_case.execute("proj_123", "user_other", "user_2")
mock_repo.save.assert_not_called()
def test_unshare_not_shared_no_save(self, mock_repo, sample_project):
"""用户未被共享时不触发 save"""
mock_repo.find_by_id.return_value = sample_project
use_case = UnshareProjectUseCase(mock_repo)
result = use_case.execute("proj_123", "user_1", "user_not_shared")
assert result is sample_project
mock_repo.save.assert_not_called()
class TestDeleteProjectUseCase:
"""DeleteProjectUseCase 测试"""
def test_delete_owner_success(self, mock_repo, sample_project):
"""所有者删除成功"""
mock_repo.find_by_id.return_value = sample_project
mock_repo.delete.return_value = True
use_case = DeleteProjectUseCase(mock_repo)
result = use_case.execute("proj_123", "user_1")
assert result is True
mock_repo.delete.assert_called_once_with("proj_123")
def test_delete_nonexistent_returns_false(self, mock_repo):
"""删除不存在的项目返回 False"""
mock_repo.find_by_id.return_value = None
use_case = DeleteProjectUseCase(mock_repo)
result = use_case.execute("noexist", "user_1")
assert result is False
mock_repo.delete.assert_not_called()
def test_delete_not_owner_raises(self, mock_repo, sample_project):
"""非所有者删除抛出 PermissionError"""
mock_repo.find_by_id.return_value = sample_project
use_case = DeleteProjectUseCase(mock_repo)
with pytest.raises(PermissionError, match="只有项目所有者可以删除"):
use_case.execute("proj_123", "user_other")
mock_repo.delete.assert_not_called()
class TestCreateProjectCommand:
"""CreateProjectCommand 数据类测试"""
def test_command_fields(self):
"""命令对象字段正确"""
cmd = CreateProjectCommand(name="test", description="desc")
assert cmd.name == "test"
assert cmd.description == "desc"
def test_command_default_description(self):
"""description 默认空字符串"""
cmd = CreateProjectCommand(name="test")
assert cmd.description == ""
def test_command_is_dataclass(self):
"""是 dataclass"""
from dataclasses import is_dataclass
assert is_dataclass(CreateProjectCommand)