完善后台管理与测试支撑

1、为后台用户列表增加关键词和状态筛选,后端同步支持过滤与总数统计。

2、创建用户时要求填写初始密码,并补充前端校验和自动填充隔离。

3、优化项目管理列表角色展示、项目详情抽屉脏数据保护和审计详情抽屉关闭体验。

4、清理旧费用附件关联、统一测试模型注册,并更新迁移端点和注册筛选测试。
This commit is contained in:
Cheng Zhou
2026-06-04 11:12:40 +08:00
parent 90ca061db8
commit 41968384b5
17 changed files with 337 additions and 145 deletions
+3 -16
View File
@@ -70,22 +70,9 @@ async def _create_test_engine():
connect_args={"check_same_thread": False},
)
# Import all models to register them with Base metadata
from app.db.base_class import Base
import app.models.study
import app.models.api_endpoint_permission
import app.models.api_endpoint_registry
import app.models.user
import app.models.study_member
import app.models.subject
import app.models.visit
import app.models.ae
import app.models.monitoring_visit_issue
import app.models.site
import app.models.permission_template
import app.models.permission_access_log
import app.models.permission_metric_snapshot
import app.models.security_access_log
# Import the central model registry so Base metadata is complete regardless
# of test collection/import order.
from app.db.base import Base
# Replace PostgreSQL UUID type with custom GUID type for SQLite
for table in Base.metadata.tables.values():
@@ -101,41 +101,16 @@ async def test_startup_auth_read_with_permission(db_session: AsyncSession):
# ============================================================================
# 项目权限管理 (project_permissions) - 2个端点
# 项目权限管理已迁移为系统级权限
# ============================================================================
@pytest.mark.asyncio
async def test_project_permissions_get_with_permission(db_session: AsyncSession):
"""验证有权限的PM可以查询项目权限矩阵"""
study_id = uuid.uuid4()
perm = ApiEndpointPermission(
study_id=study_id,
role="PM",
endpoint_key="permissions:read",
allowed=True,
)
db_session.add(perm)
await db_session.commit()
def test_project_permissions_are_not_project_matrix_permissions():
"""项目权限配置由 system:permissions:project_config 控制,不再进入项目矩阵。"""
from app.core.api_permissions import API_ENDPOINT_PERMISSIONS, SYSTEM_PERMISSIONS
allowed = await role_has_api_permission(db_session, study_id, "PM", "permissions:read")
assert allowed is True
@pytest.mark.asyncio
async def test_project_permissions_update_with_permission(db_session: AsyncSession):
"""验证有权限的PM可以更新项目权限矩阵"""
study_id = uuid.uuid4()
perm = ApiEndpointPermission(
study_id=study_id,
role="PM",
endpoint_key="permissions:update",
allowed=True,
)
db_session.add(perm)
await db_session.commit()
allowed = await role_has_api_permission(db_session, study_id, "PM", "permissions:update")
assert allowed is True
assert "permissions:read" not in API_ENDPOINT_PERMISSIONS
assert "permissions:update" not in API_ENDPOINT_PERMISSIONS
assert "system:permissions:project_config" in SYSTEM_PERMISSIONS
# ============================================================================
@@ -506,17 +481,9 @@ async def test_startup_permission_denied_for_cra(db_session: AsyncSession):
@pytest.mark.asyncio
async def test_project_permissions_denied_for_cra(db_session: AsyncSession):
"""验证CRA无法修改项目权限"""
async def test_removed_project_permissions_are_denied_for_project_roles(db_session: AsyncSession):
"""项目角色不能再通过 permissions:update 这种残留 key 获得权限管理能力。"""
study_id = uuid.uuid4()
perm = ApiEndpointPermission(
study_id=study_id,
role="CRA",
endpoint_key="permissions:update",
allowed=False,
)
db_session.add(perm)
await db_session.commit()
allowed = await role_has_api_permission(db_session, study_id, "CRA", "permissions:update")
assert allowed is False
+58
View File
@@ -190,6 +190,64 @@ async def test_admin_can_approve_user(client_and_db):
assert refreshed.approved_by is not None
@pytest.mark.asyncio
async def test_admin_users_list_filters_by_keyword_and_status(client_and_db):
client, SessionLocal = client_and_db
async with SessionLocal() as session:
session.add_all(
[
User(
email="active-filter@test.com",
password_hash=hash_password("Password123"),
full_name="Active Filter",
clinical_department="Oncology",
status=UserStatus.ACTIVE,
),
User(
email="pending-filter@test.com",
password_hash=hash_password("Password123"),
full_name="Pending Filter",
clinical_department="Cardiology",
status=UserStatus.PENDING,
),
User(
email="disabled-filter@test.com",
password_hash=hash_password("Password123"),
full_name="Disabled Filter",
clinical_department="Oncology",
status=UserStatus.DISABLED,
),
]
)
await session.commit()
admin_login = await encrypted_login(client, "admin@test.com", "admin123")
token = admin_login.json()["access_token"]
headers = {"Authorization": f"Bearer {token}"}
active_resp = await client.get("/api/v1/users/?status=ACTIVE", headers=headers)
assert active_resp.status_code == 200
active_items = active_resp.json()["items"]
assert all(item["status"] == "ACTIVE" for item in active_items)
assert {item["email"] for item in active_items} >= {"admin@test.com", "active-filter@test.com"}
assert "pending-filter@test.com" not in {item["email"] for item in active_items}
keyword_resp = await client.get("/api/v1/users/?keyword=Oncology", headers=headers)
assert keyword_resp.status_code == 200
keyword_data = keyword_resp.json()
assert keyword_data["total"] == 2
assert {item["email"] for item in keyword_data["items"]} == {
"active-filter@test.com",
"disabled-filter@test.com",
}
combined_resp = await client.get("/api/v1/users/?keyword=Filter&status=PENDING", headers=headers)
assert combined_resp.status_code == 200
combined_data = combined_resp.json()
assert combined_data["total"] == 1
assert combined_data["items"][0]["email"] == "pending-filter@test.com"
def test_register_request_does_not_expose_role_input():
assert "role" not in UserRegisterRequest.model_fields