Files
opsxsql/backend/tests/test_system_settings.py
T
logan 8b921c6c83 refactor: mock清理+安全配置+通用组件+菜单优化
P0安全: SECRET_KEY/ADMIN_PASSWORD/CORS环境变量化, seed仅admin

P1Mock: mock.ts 301->88行, 删8数据mock+5generateMock, net-1300行

P2组件: useToast/useFetchList+ConfirmDialog/FormDialog

P3菜单: menuSections/menuItems->constants/menu.ts

P4API: NotificationsPage/WorkflowPage重写+RolesPage演示

测试: 1050passed, tsc clean
2026-05-24 02:42:29 +08:00

244 lines
8.3 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.
"""系统设置 API 测试 — GET/PUT 设置 + 公共查询设置 + 查询集成。"""
import pytest
from httpx import AsyncClient, ASGITransport
from app.main import app
@pytest.fixture
async def client():
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as c:
yield c
async def _admin_token(client: AsyncClient) -> str:
resp = await client.post("/api/v1/auth/login", json={
"username": "admin", "password": "123456"
})
assert resp.status_code == 200
return resp.json()["access_token"]
def _auth_header(token: str) -> dict:
return {"Authorization": f"Bearer {token}"}
async def _ensure_user(client: AsyncClient, admin_token: str, username: str, role: str = "developer") -> str:
"""确保用户存在并返回 token。"""
resp = await client.post("/api/v1/auth/login", json={"username": username, "password": "123456"})
if resp.status_code == 200:
return resp.json()["access_token"]
# 创建
await client.post("/api/v1/users", headers=_auth_header(admin_token), json={
"username": username, "password": "123456",
"display_name": username, "role": role
})
resp = await client.post("/api/v1/auth/login", json={"username": username, "password": "123456"})
assert resp.status_code == 200
return resp.json()["access_token"]
# ── 1. GET /settings ──
@pytest.mark.asyncio
async def test_get_settings(client):
"""管理员可以获取所有系统设置。"""
token = await _admin_token(client)
resp = await client.get("/api/v1/settings", headers=_auth_header(token))
assert resp.status_code == 200
data = resp.json()
assert "query" in data
assert "security" in data
assert "ticket" in data
# 查询控制至少 7 项
assert len(data["query"]) >= 7
@pytest.mark.asyncio
async def test_get_settings_non_admin(client):
"""非管理员不能获取设置。"""
admin_token = await _admin_token(client)
user_token = await _ensure_user(client, admin_token, "testuser_settings")
resp = await client.get("/api/v1/settings", headers=_auth_header(user_token))
# require_role("admin") 应该返回 403
assert resp.status_code == 403
# ── 2. PUT /settings ──
@pytest.mark.asyncio
async def test_update_settings_boolean(client):
"""更新布尔设置项。"""
token = await _admin_token(client)
resp = await client.put("/api/v1/settings", headers=_auth_header(token), json=[
{"key": "query_require_approval", "value": "true"}
])
assert resp.status_code == 200
assert "query_require_approval" in resp.json()["updated"]
# 验证
resp = await client.get("/api/v1/settings", headers=_auth_header(token))
items = resp.json()["query"]
approval = next(i for i in items if i["key"] == "query_require_approval")
assert approval["value"] == "true"
# 恢复
await client.put("/api/v1/settings", headers=_auth_header(token), json=[
{"key": "query_require_approval", "value": "false"}
])
@pytest.mark.asyncio
async def test_update_settings_integer(client):
"""更新整数设置项。"""
token = await _admin_token(client)
resp = await client.put("/api/v1/settings", headers=_auth_header(token), json=[
{"key": "query_max_limit", "value": "5000"}
])
assert resp.status_code == 200
# 恢复
await client.put("/api/v1/settings", headers=_auth_header(token), json=[
{"key": "query_max_limit", "value": "10000"}
])
@pytest.mark.asyncio
async def test_update_settings_validation(client):
"""设置值校验。"""
token = await _admin_token(client)
# 布尔值错误
resp = await client.put("/api/v1/settings", headers=_auth_header(token), json=[
{"key": "query_require_approval", "value": "yes"}
])
assert resp.status_code == 400
# 整数范围错误
resp = await client.put("/api/v1/settings", headers=_auth_header(token), json=[
{"key": "query_max_limit", "value": "10"}
])
assert resp.status_code == 400
# 超时范围错误
resp = await client.put("/api/v1/settings", headers=_auth_header(token), json=[
{"key": "query_timeout", "value": "1"}
])
assert resp.status_code == 400
# ── 3. GET /settings/public/query ──
@pytest.mark.asyncio
async def test_public_query_settings(client):
"""公共查询设置无需鉴权。"""
resp = await client.get("/api/v1/settings/public/query")
assert resp.status_code == 200
data = resp.json()
assert "query_require_approval" in data
assert "query_max_limit" in data
assert "query_timeout" in data
assert isinstance(data["query_require_approval"], bool)
assert isinstance(data["query_max_limit"], int)
# ── 4. 查询集成 — 审批开关 ──
@pytest.mark.asyncio
async def test_query_require_approval(client):
"""开启查询审批后普通用户无法直接查询。"""
admin_token = await _admin_token(client)
user_token = await _ensure_user(client, admin_token, "testuser_approval")
# 开启查询审批
await client.put("/api/v1/settings", headers=_auth_header(admin_token), json=[
{"key": "query_require_approval", "value": "true"}
])
# 普通用户查询应被拦截
resp = await client.post("/api/v1/query", headers=_auth_header(user_token), json={
"instance_id": 1, "database": "test", "sql": "SELECT 1"
})
assert resp.status_code == 403
assert "审批" in resp.json()["detail"]
# 管理员仍然可以查询(不被审批限制拦截,可能因连接报其他错)
resp = await client.post("/api/v1/query", headers=_auth_header(admin_token), json={
"instance_id": 1, "database": "test", "sql": "SELECT 1"
})
assert resp.status_code != 403, "管理员不应被审批限制拦截"
# 恢复
await client.put("/api/v1/settings", headers=_auth_header(admin_token), json=[
{"key": "query_require_approval", "value": "false"}
])
# ── 5. 查询集成 — 语句长度检查 ──
@pytest.mark.asyncio
async def test_query_max_length(client):
"""超长查询语句被拦截。"""
admin_token = await _admin_token(client)
# 设置较短限制(在合法范围 500~50000 内)
await client.put("/api/v1/settings", headers=_auth_header(admin_token), json=[
{"key": "query_max_length", "value": "500"}
])
# 超长 SQL(> 500 字符)
long_sql = "SELECT * FROM users WHERE id = " + "1" * 600
resp = await client.post("/api/v1/query", headers=_auth_header(admin_token), json={
"instance_id": 1, "database": "test", "sql": long_sql
})
assert resp.status_code == 400, f"Expected 400, got {resp.status_code}: {resp.text}"
assert "过长" in resp.json()["detail"]
# 正常长度 SQL 不被拦截(可能因连接失败但不应是400长度错误)
short_sql = "SELECT 1"
resp = await client.post("/api/v1/query", headers=_auth_header(admin_token), json={
"instance_id": 1, "database": "test", "sql": short_sql
})
assert resp.status_code != 400 or "过长" not in resp.text
# 恢复
await client.put("/api/v1/settings", headers=_auth_header(admin_token), json=[
{"key": "query_max_length", "value": "50000"}
])
# ── 6. 批量更新 ──
@pytest.mark.asyncio
async def test_bulk_update(client):
"""批量更新多个设置。"""
token = await _admin_token(client)
resp = await client.put("/api/v1/settings", headers=_auth_header(token), json=[
{"key": "query_timeout", "value": "60"},
{"key": "query_max_limit", "value": "2000"},
])
assert resp.status_code == 200
assert len(resp.json()["updated"]) == 2
# 恢复
await client.put("/api/v1/settings", headers=_auth_header(token), json=[
{"key": "query_timeout", "value": "30"},
{"key": "query_max_limit", "value": "10000"},
])
# ── 7. 设置种子数据自动初始化 ──
@pytest.mark.asyncio
async def test_settings_auto_seed(client):
"""设置接口自动初始化种子数据。"""
token = await _admin_token(client)
resp = await client.get("/api/v1/settings", headers=_auth_header(token))
assert resp.status_code == 200
data = resp.json()
# 确认至少 12 个设置项
total = sum(len(v) for v in data.values())
assert total >= 12