Files
QMDSearch/tests/test_user_management_integration.py
T
kplam 6c6f690788 chore: 完成全量功能迭代与部署准备
- 移除冗余依赖包
- 新增账号禁用校验与用户管理能力
- 新增文档下载与管理页面文件展示
- 新增API文档页面与用户管理前端页面
- 重构时区处理与docker-compose部署配置
- 完善测试用例与项目文档
2026-08-01 00:02:42 +08:00

248 lines
8.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Spec Task 5:用户管理增强(PATCH 端点 + enabled 字段)全链路集成验证
内存 UserStore/SessionStore 经 conftest.auth_stores 夹具注入 app.api.deps 单例,
通过 TestClient 走真实 HTTP 链路(不跑 lifespan,无需真实 Redis/Qdrant),覆盖:
- 改角色生效:admin 创建 user → PATCH 改 admin → GET /auth/me 与重新登录均反映新角色
- 禁用用户:旧 token 立即失效(session 已清,1005);重新登录 1005("账号已禁用"
- 重新启用:PATCH enabled=True → login 恢复成功
- 最后 admin 保护:唯一 admin 降级/禁用自己 → 1001(且自身状态未变)
- PATCH 校验:空 body / 非法 role → 1001;不存在用户 → 1004;非 admin 调用 → 1006
"""
from typing import Any
from fastapi.testclient import TestClient
from app.main import app
def _login(client: TestClient, username: str, password: str) -> dict[str, Any]:
"""调登录接口并返回响应体"""
return client.post("/api/v1/auth/login", json={"username": username, "password": password}).json()
def _bearer(token: str) -> dict[str, str]:
"""构造 Authorization Bearer 请求头"""
return {"Authorization": f"Bearer {token}"}
def _admin_login(client: TestClient) -> dict[str, str]:
"""以 auth_stores 预置的 adminadmin/admin-pass-123)登录,返回 Bearer 请求头"""
resp = _login(client, "admin", "admin-pass-123")
assert resp["code"] == 0, f"admin 登录失败: {resp}"
return _bearer(resp["data"]["token"])
def _create_user(
client: TestClient,
admin_headers: dict[str, str],
username: str,
password: str,
role: str = "user",
) -> None:
"""admin 创建用户并断言成功"""
resp = client.post(
"/api/v1/auth/users",
json={"username": username, "password": password, "role": role},
headers=admin_headers,
).json()
assert resp["code"] == 0, f"创建用户 {username} 失败: {resp}"
class TestRoleChange:
"""改角色生效全链路"""
def test_role_change_takes_effect(self, auth_stores) -> None:
client = TestClient(app)
admin_headers = _admin_login(client)
# admin 创建 user 角色账号 alice
_create_user(client, admin_headers, "alice", "alice-pass-123", role="user")
# alice 登录,初始角色 user
alice_login = _login(client, "alice", "alice-pass-123")
assert alice_login["code"] == 0
assert alice_login["data"]["role"] == "user"
alice_headers = _bearer(alice_login["data"]["token"])
# 改角色前 GET /auth/me 反映 user 角色
me_before = client.get("/api/v1/auth/me", headers=alice_headers).json()
assert me_before["code"] == 0
assert me_before["data"]["role"] == "user"
# admin PATCH 改 alice 角色为 admin
patched = client.patch(
"/api/v1/auth/users/alice",
json={"role": "admin"},
headers=admin_headers,
).json()
assert patched["code"] == 0
assert patched["data"]["role"] == "admin"
# 同一 token 立即反映新角色(角色变更不清 session,用户记录实时读取)
me_after = client.get("/api/v1/auth/me", headers=alice_headers).json()
assert me_after["code"] == 0
assert me_after["data"]["role"] == "admin"
# 重新登录也反映新角色
relogin = _login(client, "alice", "alice-pass-123")
assert relogin["code"] == 0
assert relogin["data"]["role"] == "admin"
class TestDisableUser:
"""禁用用户:旧 session 清除 + 登录拒绝"""
def test_disable_user_clears_session_and_blocks_login(self, auth_stores) -> None:
client = TestClient(app)
admin_headers = _admin_login(client)
_create_user(client, admin_headers, "bob", "bob-pass-123")
# bob 登录拿 token
bob_login = _login(client, "bob", "bob-pass-123")
assert bob_login["code"] == 0
bob_headers = _bearer(bob_login["data"]["token"])
# admin 禁用 bob(清空其全部 session
disabled = client.patch(
"/api/v1/auth/users/bob",
json={"enabled": False},
headers=admin_headers,
).json()
assert disabled["code"] == 0
assert disabled["data"]["enabled"] is False
# bob 旧 token 调鉴权端点 → 1005(session 已清,凭证无效)
me = client.get("/api/v1/auth/me", headers=bob_headers).json()
assert me["code"] == 1005
# bob 重新登录 → 1005(账号已禁用)
relogin = _login(client, "bob", "bob-pass-123")
assert relogin["code"] == 1005
assert "禁用" in relogin["message"]
class TestReenableUser:
"""重新启用:login 恢复成功"""
def test_reenable_user_allows_login(self, auth_stores) -> None:
client = TestClient(app)
admin_headers = _admin_login(client)
_create_user(client, admin_headers, "carol", "carol-pass-123")
# 先禁用 carol,确认登录被拒
disabled = client.patch(
"/api/v1/auth/users/carol",
json={"enabled": False},
headers=admin_headers,
).json()
assert disabled["code"] == 0
assert _login(client, "carol", "carol-pass-123")["code"] == 1005
# 重新启用
enabled = client.patch(
"/api/v1/auth/users/carol",
json={"enabled": True},
headers=admin_headers,
).json()
assert enabled["code"] == 0
assert enabled["data"]["enabled"] is True
# login 恢复成功
relogin = _login(client, "carol", "carol-pass-123")
assert relogin["code"] == 0
assert relogin["data"]["username"] == "carol"
class TestLastAdminProtection:
"""最后 admin 保护:唯一 admin 不可降级/禁用"""
def test_cannot_demote_last_admin(self, auth_stores) -> None:
client = TestClient(app)
admin_headers = _admin_login(client)
# 唯一 adminadmin)尝试降级自己 role=user → 1001
resp = client.patch(
"/api/v1/auth/users/admin",
json={"role": "user"},
headers=admin_headers,
).json()
assert resp["code"] == 1001
# admin 未被降级,仍可访问 admin 专属端点
assert client.get("/api/v1/auth/users", headers=admin_headers).json()["code"] == 0
def test_cannot_disable_last_admin(self, auth_stores) -> None:
client = TestClient(app)
admin_headers = _admin_login(client)
# 唯一 admin 尝试禁用自己 enabled=False → 1001
resp = client.patch(
"/api/v1/auth/users/admin",
json={"enabled": False},
headers=admin_headers,
).json()
assert resp["code"] == 1001
# admin 未被禁用,旧 token 仍可用
assert client.get("/api/v1/auth/users", headers=admin_headers).json()["code"] == 0
class TestPatchValidation:
"""PATCH 端点校验:空 body / 非法 role / 不存在用户 / 非 admin"""
def test_empty_body_rejected(self, auth_stores) -> None:
client = TestClient(app)
admin_headers = _admin_login(client)
_create_user(client, admin_headers, "dave", "dave-pass-123")
# 空 bodyrole 与 enabled 均缺)→ 模型校验 1001
resp = client.patch(
"/api/v1/auth/users/dave",
json={},
headers=admin_headers,
).json()
assert resp["code"] == 1001
def test_invalid_role_rejected(self, auth_stores) -> None:
client = TestClient(app)
admin_headers = _admin_login(client)
_create_user(client, admin_headers, "dave", "dave-pass-123")
# role 非法(非 admin/user)→ 模型校验 1001
resp = client.patch(
"/api/v1/auth/users/dave",
json={"role": "superuser"},
headers=admin_headers,
).json()
assert resp["code"] == 1001
def test_nonexistent_user_rejected(self, auth_stores) -> None:
client = TestClient(app)
admin_headers = _admin_login(client)
# 不存在用户 → 1004
resp = client.patch(
"/api/v1/auth/users/ghost",
json={"role": "admin"},
headers=admin_headers,
).json()
assert resp["code"] == 1004
def test_non_admin_forbidden(self, auth_stores) -> None:
client = TestClient(app)
admin_headers = _admin_login(client)
_create_user(client, admin_headers, "eve", "eve-pass-123", role="user")
# eveuser 角色)登录后调 PATCH → 1006
eve_login = _login(client, "eve", "eve-pass-123")
assert eve_login["code"] == 0
eve_headers = _bearer(eve_login["data"]["token"])
resp = client.patch(
"/api/v1/auth/users/eve",
json={"role": "admin"},
headers=eve_headers,
).json()
assert resp["code"] == 1006