Files
opsxsql/backend/tests/test_system_settings.py
T
logan be2d6f3894 feat: LDAP 账号管理 — OU管理/添加用户/删除用户/批量导入
后端新增 4 个 LDAP 管理端点:
- POST /ldap-config/create-ou: 创建组织单元
- POST /ldap-config/create-user: 创建单个用户 (SSHA加密)
- POST /ldap-config/delete-user: 删除用户
- POST /ldap-config/batch-import-ldap: 批量导入用户
- POST /ldap-config/user-list: 查询用户列表(恢复)

前端 LdapConfigPage 增强:
- LDAP 账号管理折叠区(配置存在即显示)
- OU 下拉选择 + 根节点选项 + 数量统计
- 用户列表表格(全选/单选/DN显示)
- 新建 OU 弹框(名称+父级DN+描述)
- 添加用户弹框(用户名+密码+显示名+邮箱+目标OU)
- 删除选中用户按钮(确认后批量删除)

前端 UsersPage BatchImportModal 三 Tab:
- 本地导入 / 导入到 LDAP / 从 LDAP 导入

测试修复:
- test_user_endpoint.py: 分页格式兼容
- test_users.py: 分页格式兼容
- test_system_settings.py: 精简后配置项匹配
2026-05-25 17:34:53 +08:00

229 lines
8.0 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
from tests.conftest import auth_header
async def _ensure_user(client: AsyncClient, admin_token: str, username: str, role: str = "developer") -> str:
"""确保用户存在并返回 token。"""
from app.core.security import hash_password, create_access_token
from app.models.user import User
from sqlalchemy import select
from app.core.database import get_db
# 通过 API 创建用户
resp = await client.post("/api/v1/users", headers=auth_header(admin_token), json={
"username": username, "password": "123456",
"display_name": username, "role": role
})
if resp.status_code in (200, 201):
data = resp.json()
user_id = data.get("id")
return create_access_token(data={"sub": str(user_id), "role": role})
# 如果已存在,尝试登录
resp = await client.post("/api/v1/auth/login", json={"username": username, "password": "123456"})
if resp.status_code == 200:
return resp.json()["access_token"]
# Fallback: 直接生成 token
raise RuntimeError(f"无法创建/获取用户 {username}: {resp.status_code} {resp.text}")
# ── 1. GET /settings ──
@pytest.mark.asyncio
async def test_get_settings(client, admin_token):
"""管理员可以获取所有系统设置。"""
token = admin_token
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
# 查询控制至少 3 项 (max_limit, timeout, auto_limit)
assert len(data["query"]) >= 3
@pytest.mark.asyncio
async def test_get_settings_non_admin(client, admin_token):
"""非管理员不能获取设置。"""
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, admin_token):
"""更新布尔设置项。"""
token = admin_token
resp = await client.put("/api/v1/settings", headers=auth_header(token), json=[
{"key": "query_auto_limit", "value": "false"}
])
assert resp.status_code == 200
assert "query_auto_limit" in resp.json()["updated"]
# 验证
resp = await client.get("/api/v1/settings", headers=auth_header(token))
items = resp.json()["query"]
auto_limit = next(i for i in items if i["key"] == "query_auto_limit")
assert auto_limit["value"] == "false"
# 恢复
await client.put("/api/v1/settings", headers=auth_header(token), json=[
{"key": "query_auto_limit", "value": "true"}
])
@pytest.mark.asyncio
async def test_update_settings_integer(client, admin_token):
"""更新整数设置项。"""
token = admin_token
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, admin_token):
"""设置值校验。"""
token = admin_token
# 布尔值错误
resp = await client.put("/api/v1/settings", headers=auth_header(token), json=[
{"key": "query_auto_limit", "value": "yes"}
])
assert resp.status_code == 400
# 整数范围错误 (query_max_limit min=100)
resp = await client.put("/api/v1/settings", headers=auth_header(token), json=[
{"key": "query_max_limit", "value": "10"}
])
assert resp.status_code == 400
# 超时范围错误 (query_timeout min=5)
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_max_limit" in data
assert "query_timeout" in data
assert isinstance(data["query_max_limit"], int)
# ── 4. 查询集成 — 审批开关 ──
@pytest.mark.asyncio
async def test_query_require_approval(client, admin_token):
"""开启查询审批后普通用户无法直接查询。"""
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):
"""超长查询语句被拦截。"""
# 设置较短限制(在合法范围 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, admin_token):
"""批量更新多个设置。"""
token = admin_token
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, admin_token):
"""设置接口自动初始化种子数据。"""
token = admin_token
resp = await client.get("/api/v1/settings", headers=auth_header(token))
assert resp.status_code == 200
data = resp.json()
# 确认至少 9 个设置项 (query:3 + security:2 + cleanup:3 + executor:1)
total = sum(len(v) for v in data.values())
assert total >= 9