PMFlow/backend/tests/test_projects_member_assign...

204 lines
7.3 KiB
Python

import os
import unittest
from tempfile import NamedTemporaryFile
from unittest.mock import AsyncMock, patch
from fastapi import HTTPException
from sqlalchemy import and_, select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from app.api.projects import (
ProjectCreateRequest,
ProjectMembersAssignRequest,
assign_project_members,
create_project,
)
from app.models import Base, Notification, Project, User, UserProject
class ProjectsMemberAssignmentApiTest(unittest.IsolatedAsyncioTestCase):
async def asyncSetUp(self) -> None:
tmp = NamedTemporaryFile(prefix="pmflow-project-members-test-", suffix=".db", delete=False)
tmp.close()
self.db_path = tmp.name
self.engine = create_async_engine(f"sqlite+aiosqlite:///{self.db_path}", future=True)
self.session_factory = async_sessionmaker(self.engine, expire_on_commit=False, class_=AsyncSession)
async with self.engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
async def asyncTearDown(self) -> None:
await self.engine.dispose()
if getattr(self, "db_path", None):
try:
os.remove(self.db_path)
except FileNotFoundError:
pass
async def _seed_users(self, db: AsyncSession) -> tuple[User, User, User]:
boss = User(
tenant_id=1,
username="boss-member-test",
email="boss-member-test@example.com",
password_hash="x",
name="Boss",
role="BOSS",
status="active",
is_active=True,
)
member_a = User(
tenant_id=1,
username="member-a-test",
email="member-a-test@example.com",
password_hash="x",
name="Member A",
role="BE",
status="active",
is_active=True,
)
member_b = User(
tenant_id=1,
username="member-b-test",
email="member-b-test@example.com",
password_hash="x",
name="Member B",
role="FE",
status="active",
is_active=True,
)
db.add_all([boss, member_a, member_b])
await db.flush()
return boss, member_a, member_b
async def test_create_project_assigns_requested_members_and_owner(self) -> None:
async with self.session_factory() as db:
boss, member_a, member_b = await self._seed_users(db)
with (
patch("app.api.projects.context_cache_service.invalidate_system_context", AsyncMock()),
patch("app.api.projects.AIPMOKernelService.invalidate_world_model_cache", AsyncMock()),
):
payload = await create_project(
body=ProjectCreateRequest(
title="成员分配测试项目",
member_user_ids=[int(member_a.id), int(member_b.id)],
),
db=db,
current_user=boss,
)
project_id = int(payload["id"])
assigned_rows = (
await db.execute(
select(UserProject.user_id).where(
and_(
UserProject.tenant_id == 1,
UserProject.project_id == project_id,
)
)
)
).scalars().all()
self.assertEqual(
sorted(int(uid) for uid in assigned_rows),
sorted([int(boss.id), int(member_a.id), int(member_b.id)]),
)
async def test_assign_project_members_empty_should_notify_and_raise(self) -> None:
async with self.session_factory() as db:
boss, member_a, _ = await self._seed_users(db)
project = Project(
tenant_id=1,
name="P-members-empty",
title="P-members-empty",
description="members test",
owner_id=int(boss.id),
status="active",
assigned_role="backend",
signal_count=0,
)
db.add(project)
await db.flush()
db.add(
UserProject(
tenant_id=1,
user_id=int(member_a.id),
project_id=int(project.id),
)
)
await db.commit()
with self.assertRaises(HTTPException) as exc_ctx:
await assign_project_members(
project_id=int(project.id),
body=ProjectMembersAssignRequest(member_user_ids=[]),
db=db,
current_user=boss,
)
self.assertEqual(exc_ctx.exception.status_code, 400)
notifications = (
await db.execute(
select(Notification).where(
and_(
Notification.tenant_id == 1,
Notification.user_id == int(boss.id),
Notification.title == "项目成员分配失败",
)
)
)
).scalars().all()
self.assertGreaterEqual(len(notifications), 1)
async def test_assign_project_members_replaces_member_list(self) -> None:
async with self.session_factory() as db:
boss, member_a, member_b = await self._seed_users(db)
project = Project(
tenant_id=1,
name="P-members-replace",
title="P-members-replace",
description="members replace test",
owner_id=int(boss.id),
status="active",
assigned_role="backend",
signal_count=0,
)
db.add(project)
await db.flush()
db.add(
UserProject(
tenant_id=1,
user_id=int(boss.id),
project_id=int(project.id),
)
)
await db.commit()
with (
patch("app.api.projects.context_cache_service.invalidate_system_context", AsyncMock()),
patch("app.api.projects.AIPMOKernelService.invalidate_world_model_cache", AsyncMock()),
):
payload = await assign_project_members(
project_id=int(project.id),
body=ProjectMembersAssignRequest(member_user_ids=[int(member_a.id), int(member_b.id)]),
db=db,
current_user=boss,
)
self.assertEqual(
sorted(payload["member_user_ids"]),
sorted([str(int(member_a.id)), str(int(member_b.id))]),
)
assigned_rows = (
await db.execute(
select(UserProject.user_id).where(
and_(
UserProject.tenant_id == 1,
UserProject.project_id == int(project.id),
)
)
)
).scalars().all()
self.assertEqual(
sorted(int(uid) for uid in assigned_rows),
sorted([int(member_a.id), int(member_b.id)]),
)