chore: 完成全量功能迭代与部署准备

- 移除冗余依赖包
- 新增账号禁用校验与用户管理能力
- 新增文档下载与管理页面文件展示
- 新增API文档页面与用户管理前端页面
- 重构时区处理与docker-compose部署配置
- 完善测试用例与项目文档
This commit is contained in:
2026-08-01 00:02:42 +08:00
parent f92eff6f65
commit 6c6f690788
19 changed files with 2000 additions and 485 deletions
+247
View File
@@ -0,0 +1,247 @@
"""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