"""Project 领域模型 + UseCase 单元测试(P3-1 第30波)。""" 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.entities import Project # ── Project 领域模型 ──────────────────────────────────────────────────────── class TestProjectDomain: """Project 领域模型单元测试。""" def test_create_project_basic(self): """创建项目基本字段正确。""" project = Project.create(owner_user_id="user_001", name="我的项目") assert project.id assert project.owner_user_id == "user_001" assert project.name == "我的项目" assert project.description == "" assert project.shared_users == [] assert project.created_at def test_create_project_with_description(self): """创建项目带描述。""" project = Project.create( owner_user_id="user_001", name="测试项目", description="这是一个测试项目", ) assert project.name == "测试项目" assert project.description == "这是一个测试项目" def test_create_project_name_stripped(self): """项目名称首尾空格被清理。""" project = Project.create(owner_user_id="user_001", name=" 我的项目 ") assert project.name == "我的项目" def test_create_project_description_stripped(self): """项目描述首尾空格被清理。""" project = Project.create( owner_user_id="user_001", name="测试", description=" 描述内容 ", ) assert project.description == "描述内容" def test_create_project_empty_name_raises(self): """空项目名抛 ValueError。""" with pytest.raises(ValueError, match="项目名称不能为空"): Project.create(owner_user_id="user_001", name="") def test_create_project_whitespace_name_raises(self): """全空格项目名抛 ValueError。""" with pytest.raises(ValueError, match="项目名称不能为空"): Project.create(owner_user_id="user_001", name=" ") def test_is_owner_true(self): """is_owner 所有者返回 True。""" project = Project.create(owner_user_id="user_001", name="项目") assert project.is_owner("user_001") is True def test_is_owner_false(self): """is_owner 非所有者返回 False。""" project = Project.create(owner_user_id="user_001", name="项目") assert project.is_owner("user_002") is False def test_is_shared_with_true(self): """is_shared_with 已共享用户返回 True。""" project = Project.create(owner_user_id="user_001", name="项目") project.shared_users = ["user_002", "user_003"] assert project.is_shared_with("user_002") is True assert project.is_shared_with("user_003") is True def test_is_shared_with_false(self): """is_shared_with 未共享用户返回 False。""" project = Project.create(owner_user_id="user_001", name="项目") project.shared_users = ["user_002"] assert project.is_shared_with("user_004") is False def test_is_shared_with_empty_list(self): """共享列表为空时返回 False。""" project = Project.create(owner_user_id="user_001", name="项目") assert project.is_shared_with("user_002") is False def test_can_access_owner(self): """所有者可以访问。""" project = Project.create(owner_user_id="user_001", name="项目") assert project.can_access("user_001") is True def test_can_access_shared_user(self): """共享用户可以访问。""" project = Project.create(owner_user_id="user_001", name="项目") project.shared_users = ["user_002"] assert project.can_access("user_002") is True def test_can_access_other_user(self): """其他用户不能访问。""" project = Project.create(owner_user_id="user_001", name="项目") project.shared_users = ["user_002"] assert project.can_access("user_003") is False # ── ListProjectsUseCase ────────────────────────────────────────────────────── class TestListProjectsUseCase: """ListProjectsUseCase 单元测试。""" def test_list_projects_success(self): """正常列出用户项目。""" mock_repo = MagicMock() mock_repo.find_accessible_projects.return_value = [ Project.create(owner_user_id="user_001", name="项目1"), Project.create(owner_user_id="user_001", name="项目2"), ] use_case = ListProjectsUseCase(mock_repo) result = use_case.execute("user_001") assert len(result) == 2 mock_repo.find_accessible_projects.assert_called_once_with("user_001") def test_list_projects_empty(self): """用户没有项目返回空列表。""" mock_repo = MagicMock() mock_repo.find_accessible_projects.return_value = [] use_case = ListProjectsUseCase(mock_repo) result = use_case.execute("user_001") assert result == [] def test_list_projects_empty_user_id_raises(self): """空 user_id 抛 ValueError。""" mock_repo = MagicMock() use_case = ListProjectsUseCase(mock_repo) with pytest.raises(ValueError, match="user_id 不能为空"): use_case.execute("") def test_list_projects_whitespace_user_id_raises(self): """全空格 user_id 抛 ValueError。""" mock_repo = MagicMock() use_case = ListProjectsUseCase(mock_repo) with pytest.raises(ValueError, match="user_id 不能为空"): use_case.execute(" ") def test_list_projects_user_id_stripped(self): """user_id 首尾空格被清理。""" mock_repo = MagicMock() mock_repo.find_accessible_projects.return_value = [] use_case = ListProjectsUseCase(mock_repo) use_case.execute(" user_001 ") mock_repo.find_accessible_projects.assert_called_once_with("user_001") # ── GetProjectUseCase ─────────────────────────────────────────────────────── class TestGetProjectUseCase: """GetProjectUseCase 单元测试。""" def test_get_project_success(self): """正常获取项目。""" project = Project.create(owner_user_id="user_001", name="测试项目") mock_repo = MagicMock() mock_repo.find_by_id.return_value = project use_case = GetProjectUseCase(mock_repo) result = use_case.execute("proj_001") assert result == project mock_repo.find_by_id.assert_called_once_with("proj_001") def test_get_project_not_found(self): """项目不存在返回 None。""" mock_repo = MagicMock() mock_repo.find_by_id.return_value = None use_case = GetProjectUseCase(mock_repo) result = use_case.execute("proj_001") assert result is None def test_get_project_empty_id_raises(self): """空 project_id 抛 ValueError。""" mock_repo = MagicMock() use_case = GetProjectUseCase(mock_repo) with pytest.raises(ValueError, match="project_id 不能为空"): use_case.execute("") def test_get_project_whitespace_id_raises(self): """全空格 project_id 抛 ValueError。""" mock_repo = MagicMock() use_case = GetProjectUseCase(mock_repo) with pytest.raises(ValueError, match="project_id 不能为空"): use_case.execute(" ") def test_get_project_id_stripped(self): """project_id 首尾空格被清理。""" mock_repo = MagicMock() mock_repo.find_by_id.return_value = None use_case = GetProjectUseCase(mock_repo) use_case.execute(" proj_001 ") mock_repo.find_by_id.assert_called_once_with("proj_001") # ── CreateProjectUseCase ──────────────────────────────────────────────────── class TestCreateProjectUseCase: """CreateProjectUseCase 单元测试。""" def test_create_project_success(self): """正常创建项目。""" mock_repo = MagicMock() mock_repo.save.side_effect = lambda p: p use_case = CreateProjectUseCase(mock_repo) command = CreateProjectCommand(name="新项目", description="项目描述") result = use_case.execute(command, "user_001") assert result.name == "新项目" assert result.description == "项目描述" assert result.owner_user_id == "user_001" mock_repo.save.assert_called_once() def test_create_project_default_description(self): """不传描述默认为空。""" mock_repo = MagicMock() mock_repo.save.side_effect = lambda p: p use_case = CreateProjectUseCase(mock_repo) command = CreateProjectCommand(name="新项目") result = use_case.execute(command, "user_001") assert result.description == "" def test_create_project_empty_name_raises(self): """空项目名抛 ValueError(领域层校验)。""" mock_repo = MagicMock() use_case = CreateProjectUseCase(mock_repo) command = CreateProjectCommand(name="") with pytest.raises(ValueError, match="项目名称不能为空"): use_case.execute(command, "user_001") mock_repo.save.assert_not_called() # ── ShareProjectUseCase ───────────────────────────────────────────────────── class TestShareProjectUseCase: """ShareProjectUseCase 单元测试。""" def test_share_project_success(self): """正常共享项目给用户。""" project = Project.create(owner_user_id="user_001", name="项目") mock_repo = MagicMock() mock_repo.find_by_id.return_value = project mock_repo.save.side_effect = lambda p: p use_case = ShareProjectUseCase(mock_repo) result = use_case.execute(project.id, "user_001", "user_002") assert "user_002" in result.shared_users mock_repo.save.assert_called_once() def test_share_project_already_shared_no_duplicate(self): """已共享用户再次共享不重复添加。""" project = Project.create(owner_user_id="user_001", name="项目") project.shared_users = ["user_002"] mock_repo = MagicMock() mock_repo.find_by_id.return_value = project mock_repo.save.side_effect = lambda p: p use_case = ShareProjectUseCase(mock_repo) result = use_case.execute(project.id, "user_001", "user_002") assert result.shared_users == ["user_002"] mock_repo.save.assert_not_called() def test_share_project_not_found_raises(self): """项目不存在抛 ValueError。""" mock_repo = MagicMock() mock_repo.find_by_id.return_value = None use_case = ShareProjectUseCase(mock_repo) with pytest.raises(ValueError, match="项目不存在"): use_case.execute("proj_xxx", "user_001", "user_002") mock_repo.save.assert_not_called() def test_share_project_not_owner_raises(self): """非所有者不能共享项目。""" project = Project.create(owner_user_id="user_001", name="项目") mock_repo = MagicMock() mock_repo.find_by_id.return_value = project use_case = ShareProjectUseCase(mock_repo) with pytest.raises(ValueError, match="只有项目所有者可以共享项目"): use_case.execute(project.id, "user_002", "user_003") mock_repo.save.assert_not_called() # ── UnshareProjectUseCase ─────────────────────────────────────────────────── class TestUnshareProjectUseCase: """UnshareProjectUseCase 单元测试。""" def test_unshare_project_success(self): """正常取消共享。""" project = Project.create(owner_user_id="user_001", name="项目") project.shared_users = ["user_002", "user_003"] mock_repo = MagicMock() mock_repo.find_by_id.return_value = project mock_repo.save.side_effect = lambda p: p use_case = UnshareProjectUseCase(mock_repo) result = use_case.execute(project.id, "user_001", "user_002") assert result.shared_users == ["user_003"] mock_repo.save.assert_called_once() def test_unshare_project_not_shared_noop(self): """未共享的用户取消共享不操作。""" project = Project.create(owner_user_id="user_001", name="项目") project.shared_users = ["user_003"] mock_repo = MagicMock() mock_repo.find_by_id.return_value = project mock_repo.save.side_effect = lambda p: p use_case = UnshareProjectUseCase(mock_repo) result = use_case.execute(project.id, "user_001", "user_002") assert result.shared_users == ["user_003"] mock_repo.save.assert_not_called() def test_unshare_project_not_found_raises(self): """项目不存在抛 ValueError。""" mock_repo = MagicMock() mock_repo.find_by_id.return_value = None use_case = UnshareProjectUseCase(mock_repo) with pytest.raises(ValueError, match="项目不存在"): use_case.execute("proj_xxx", "user_001", "user_002") mock_repo.save.assert_not_called() def test_unshare_project_not_owner_raises(self): """非所有者不能取消共享。""" project = Project.create(owner_user_id="user_001", name="项目") project.shared_users = ["user_002"] mock_repo = MagicMock() mock_repo.find_by_id.return_value = project use_case = UnshareProjectUseCase(mock_repo) with pytest.raises(ValueError, match="只有项目所有者可以取消共享"): use_case.execute(project.id, "user_002", "user_003") mock_repo.save.assert_not_called() # ── DeleteProjectUseCase ──────────────────────────────────────────────────── class TestDeleteProjectUseCase: """DeleteProjectUseCase 单元测试。""" def test_delete_project_success(self): """所有者正常删除项目。""" project = Project.create(owner_user_id="user_001", name="项目") mock_repo = MagicMock() mock_repo.find_by_id.return_value = project mock_repo.delete.return_value = True use_case = DeleteProjectUseCase(mock_repo) result = use_case.execute(project.id, "user_001") assert result is True mock_repo.delete.assert_called_once_with(project.id) def test_delete_project_not_found(self): """项目不存在返回 False。""" mock_repo = MagicMock() mock_repo.find_by_id.return_value = None use_case = DeleteProjectUseCase(mock_repo) result = use_case.execute("proj_xxx", "user_001") assert result is False mock_repo.delete.assert_not_called() def test_delete_project_not_owner_raises(self): """非所有者删除抛 PermissionError。""" project = Project.create(owner_user_id="user_001", name="项目") mock_repo = MagicMock() mock_repo.find_by_id.return_value = project use_case = DeleteProjectUseCase(mock_repo) with pytest.raises(PermissionError, match="只有项目所有者可以删除项目"): use_case.execute(project.id, "user_002") mock_repo.delete.assert_not_called() def test_delete_project_shared_user_raises(self): """共享用户不能删除项目。""" project = Project.create(owner_user_id="user_001", name="项目") project.shared_users = ["user_002"] mock_repo = MagicMock() mock_repo.find_by_id.return_value = project use_case = DeleteProjectUseCase(mock_repo) with pytest.raises(PermissionError, match="只有项目所有者可以删除项目"): use_case.execute(project.id, "user_002") mock_repo.delete.assert_not_called()