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
299 lines
10 KiB
Python
Executable File
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)
|