"""项目 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)