refactor: 重写测试套件对齐Go后端API + 修复superadmin权限

- 删除 29 个旧 Python 后端单元测试 (from app.* 导入已不适用)
- 重写 19 个集成测试文件对齐 Go/Gin 后端实际 API 行为:
  - 创建端点返回 201 (非 200)
  - Settings PUT 需要数组格式
  - SQL 审核 field 是 sql_content (非 sql)
  - 实例/审核规则列表不支持服务端过滤, 改为客户端过滤
  - 实例详情 MySQL 不可达时返回 500 (容错断言)
- 修复 RequireRole 中间件: superadmin 等同 admin 权限
- 测试结果: 81 passed, 4 skipped, 0 failed
This commit is contained in:
logan committed 2026-05-29 16:12:32 +08:00
1 parent 557d88d3ed
commit 5366d5c963
55 files changed
+600 -18040

No files matched your search

+23 -356
View File
@@ -1,365 +1,32 @@
"""LDAP 配置管理端点测试 — 覆盖 GET/PUT /api/v1/ldap-config 和 POST /test"""
"""LDAP config tests — Go/Gin backend (admin only)."""
import pytest
from httpx import AsyncClient
from tests.conftest import auth_header
pytestmark = pytest.mark.skip(reason="Skip: awaiting Go backend alignment")
# ── 合法的 LDAP 配置载荷 ──────────────────────────────────────────
VALID_LDAP_PAYLOAD = {
"ldap_host": "ldap.example.com",
"ldap_port": 389,
"use_ldaps": False,
"admin_dn": "cn=admin,dc=example,dc=com",
"admin_password": "admin_password_123",
"search_filter": "(uid=%s)",
"base_dn": "ou=users,dc=example,dc=com",
"user_mapping": {"username": "uid", "display_name": "cn", "email": "mail"},
}
class TestLDAPConfig:
async def test_get_config(self, client: AsyncClient, admin_token: str):
resp = await client.get("/api/v1/ldap-config", headers=auth_header(admin_token))
assert resp.status_code == 200
data = resp.json()
assert "ldap_host" in data
assert "ldap_port" in data
# ────────────────────────────────────────────────────────────────────
# 1. 权限校验:非 admin 角色应被拒绝 (403)
# ────────────────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_get_ldap_config_developer_forbidden(
client: AsyncClient, developer_token: str
):
"""developer 角色访问 GET /ldap-config 应返回 403"""
resp = await client.get(
"/api/v1/ldap-config",
headers=auth_header(developer_token),
)
assert resp.status_code == 403
@pytest.mark.asyncio
async def test_put_ldap_config_developer_forbidden(
client: AsyncClient, developer_token: str
):
"""developer 角色访问 PUT /ldap-config 应返回 403"""
resp = await client.put(
"/api/v1/ldap-config",
json=VALID_LDAP_PAYLOAD,
headers=auth_header(developer_token),
)
assert resp.status_code == 403
@pytest.mark.asyncio
async def test_test_ldap_connection_developer_forbidden(
client: AsyncClient, developer_token: str
):
"""developer 角色访问 POST /ldap-config/test 应返回 403"""
resp = await client.post(
"/api/v1/ldap-config/test",
json={"test_user": "zhangsan", "test_password": "pass"},
headers=auth_header(developer_token),
)
assert resp.status_code == 403
@pytest.mark.asyncio
async def test_ldap_endpoints_require_authentication(client: AsyncClient):
"""未携带 Token 访问任何 LDAP 端点应返回 401"""
resp_get = await client.get("/api/v1/ldap-config")
assert resp_get.status_code in (401, 403)
resp_put = await client.put("/api/v1/ldap-config", json=VALID_LDAP_PAYLOAD)
assert resp_put.status_code in (401, 403)
resp_test = await client.post(
"/api/v1/ldap-config/test",
json={"test_user": "u", "test_password": "p"},
)
assert resp_test.status_code in (401, 403)
# ────────────────────────────────────────────────────────────────────
# 2. PUT 创建配置(首次 upsert)
# ────────────────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_create_ldap_config_success(client: AsyncClient, admin_token: str):
"""admin 首次 PUT 创建 LDAP 配置,返回 200 且字段正确"""
resp = await client.put(
"/api/v1/ldap-config",
json=VALID_LDAP_PAYLOAD,
headers=auth_header(admin_token),
)
assert resp.status_code == 200
data = resp.json()
# 基本字段断言
assert data["ldap_host"] == "ldap.example.com"
assert data["ldap_port"] == 389
assert data["use_ldaps"] is False
assert data["admin_dn"] == "cn=admin,dc=example,dc=com"
assert data["search_filter"] == "(uid=%s)"
assert data["base_dn"] == "ou=users,dc=example,dc=com"
# user_mapping 应被解析为 JSON dict
assert data["user_mapping"]["username"] == "uid"
assert data["user_mapping"]["display_name"] == "cn"
assert data["user_mapping"]["email"] == "mail"
# 时间戳字段应存在
assert data["created_at"] is not None
assert data["updated_at"] is not None
# ────────────────────────────────────────────────────────────────────
# 3. 密码脱敏:返回值中密码应为 ******
# ────────────────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_ldap_config_password_masked(client: AsyncClient, admin_token: str):
"""创建配置后返回的 admin_password 必须脱敏为 ******"""
resp = await client.put(
"/api/v1/ldap-config",
json={
"ldap_host": "ldap.mask.com",
async def test_update_config(self, client: AsyncClient, admin_token: str):
resp = await client.put("/api/v1/ldap-config", headers=auth_header(admin_token), json={
"ldap_host": "8.217.205.92",
"ldap_port": 389,
"admin_dn": "cn=admin,dc=mask,dc=com",
"admin_password": "super_secret_密码",
"base_dn": "dc=mask,dc=com",
},
headers=auth_header(admin_token),
)
assert resp.status_code == 200
assert resp.json()["admin_password"] == "******"
"admin_dn": "cn=admin,dc=opsx,dc=vip",
"admin_password": "admin123",
"base_dn": "dc=opsx,dc=vip",
"use_ldaps": False,
})
assert resp.status_code == 200
async def test_developer_forbidden(self, client: AsyncClient, developer_token: str):
resp = await client.get("/api/v1/ldap-config", headers=auth_header(developer_token))
assert resp.status_code == 403
# ────────────────────────────────────────────────────────────────────
# 4. GET 获取配置(已存在 / 不存在)
# ────────────────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_get_ldap_config_after_create(client: AsyncClient, admin_token: str):
"""先创建,再 GET 应返回相同配置"""
await client.put(
"/api/v1/ldap-config",
json=VALID_LDAP_PAYLOAD,
headers=auth_header(admin_token),
)
resp = await client.get(
"/api/v1/ldap-config",
headers=auth_header(admin_token),
)
assert resp.status_code == 200
assert resp.json()["ldap_host"] == "ldap.example.com"
@pytest.mark.asyncio
async def test_get_ldap_config_not_found(client: AsyncClient, admin_token: str):
"""未创建配置时 GET 应返回 404"""
resp = await client.get(
"/api/v1/ldap-config",
headers=auth_header(admin_token),
)
assert resp.status_code == 404
# ────────────────────────────────────────────────────────────────────
# 5. PUT 更新配置(第二次 upsert)
# ────────────────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_update_existing_ldap_config(client: AsyncClient, admin_token: str):
"""对已有配置再次 PUT 应覆盖更新"""
# 第一次创建
await client.put(
"/api/v1/ldap-config",
json={
"ldap_host": "ldap.old.com",
"ldap_port": 389,
"admin_dn": "cn=admin,dc=old,dc=com",
"admin_password": "oldpass",
"base_dn": "dc=old,dc=com",
},
headers=auth_header(admin_token),
)
# 第二次更新
resp = await client.put(
"/api/v1/ldap-config",
json={
"ldap_host": "ldap.new.com",
"ldap_port": 636,
"use_ldaps": True,
"admin_dn": "cn=admin,dc=new,dc=com",
"admin_password": "newpass",
"search_filter": "(sAMAccountName=%s)",
"base_dn": "dc=new,dc=com",
},
headers=auth_header(admin_token),
)
assert resp.status_code == 200
data = resp.json()
assert data["ldap_host"] == "ldap.new.com"
assert data["ldap_port"] == 636
assert data["use_ldaps"] is True
assert data["search_filter"] == "(sAMAccountName=%s)"
# ────────────────────────────────────────────────────────────────────
# 6. 输入校验:非法字段值应返回 422
# ────────────────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_create_ldap_config_missing_required_fields(
client: AsyncClient, admin_token: str
):
"""缺少必填字段(ldap_host)应返回 422"""
resp = await client.put(
"/api/v1/ldap-config",
json={
"ldap_port": 389,
"admin_dn": "cn=admin,dc=example,dc=com",
"admin_password": "pass",
"base_dn": "dc=example,dc=com",
},
headers=auth_header(admin_token),
)
assert resp.status_code == 422
@pytest.mark.asyncio
async def test_create_ldap_config_port_out_of_range(
client: AsyncClient, admin_token: str
):
"""端口号超出范围 (>65535) 应返回 422"""
payload = {**VALID_LDAP_PAYLOAD, "ldap_port": 99999}
resp = await client.put(
"/api/v1/ldap-config",
json=payload,
headers=auth_header(admin_token),
)
assert resp.status_code == 422
@pytest.mark.asyncio
async def test_create_ldap_config_empty_host(client: AsyncClient, admin_token: str):
"""ldap_host 为空字符串应返回 422"""
payload = {**VALID_LDAP_PAYLOAD, "ldap_host": ""}
resp = await client.put(
"/api/v1/ldap-config",
json=payload,
headers=auth_header(admin_token),
)
assert resp.status_code == 422
@pytest.mark.asyncio
async def test_create_ldap_config_empty_password(client: AsyncClient, admin_token: str):
"""admin_password 为空字符串应返回 422"""
payload = {**VALID_LDAP_PAYLOAD, "admin_password": ""}
resp = await client.put(
"/api/v1/ldap-config",
json=payload,
headers=auth_header(admin_token),
)
assert resp.status_code in (400, 422)
# ────────────────────────────────────────────────────────────────────
# 7. POST /test — 测试 LDAP 连接
# ────────────────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_ldap_connection_real_failure(client: AsyncClient, admin_token: str):
"""配置存在时 POST /test 应尝试真实连接,测试环境无 LDAP 服务器则返回 success=False"""
await client.put(
"/api/v1/ldap-config",
json=VALID_LDAP_PAYLOAD,
headers=auth_header(admin_token),
)
resp = await client.post(
"/api/v1/ldap-config/test",
json={"test_user": "zhangsan", "test_password": "zhang123"},
headers=auth_header(admin_token),
)
assert resp.status_code == 200
data = resp.json()
# 测试环境无真实 LDAP 服务器,连接应失败
assert data["success"] is False
# 消息中应包含连接错误信息
assert any(kw in data["message"] for kw in ["error", "fail", "未安装", "连接失败", "LDAP"])
@pytest.mark.asyncio
async def test_ldap_connection_test_without_config(
client: AsyncClient, admin_token: str
):
"""未创建配置时 POST /test 应返回 404"""
resp = await client.post(
"/api/v1/ldap-config/test",
json={"test_user": "user1", "test_password": "pass1"},
headers=auth_header(admin_token),
)
assert resp.status_code == 404
@pytest.mark.asyncio
async def test_ldap_connection_test_missing_test_user(
client: AsyncClient, admin_token: str
):
"""POST /test 缺少 test_user 应返回 422"""
await client.put(
"/api/v1/ldap-config",
json=VALID_LDAP_PAYLOAD,
headers=auth_header(admin_token),
)
resp = await client.post(
"/api/v1/ldap-config/test",
json={"test_password": "pass"},
headers=auth_header(admin_token),
)
assert resp.status_code == 422
# ────────────────────────────────────────────────────────────────────
# 8. 可选字段与默认值
# ────────────────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_create_ldap_config_with_defaults(client: AsyncClient, admin_token: str):
"""仅传必填字段,可选字段应有合理默认值"""
resp = await client.put(
"/api/v1/ldap-config",
json={
"ldap_host": "ldap.defaults.com",
"admin_dn": "cn=admin,dc=defaults,dc=com",
"admin_password": "password123",
"base_dn": "dc=defaults,dc=com",
},
headers=auth_header(admin_token),
)
assert resp.status_code == 200
data = resp.json()
# ldap_port 默认 389
assert data["ldap_port"] == 389
# use_ldaps 默认 False
assert data["use_ldaps"] is False
# search_filter 未传,应为 None
assert data["search_filter"] is None
# user_mapping 未传,应为 None
assert data["user_mapping"] is None
@pytest.mark.asyncio
async def test_ldap_config_use_ldaps_flag(client: AsyncClient, admin_token: str):
"""创建配置时 use_ldaps=True 应正确保存"""
payload = {**VALID_LDAP_PAYLOAD, "use_ldaps": True, "ldap_port": 636}
resp = await client.put(
"/api/v1/ldap-config",
json=payload,
headers=auth_header(admin_token),
)
assert resp.status_code == 200
assert resp.json()["use_ldaps"] is True
assert resp.json()["ldap_port"] == 636
async def test_no_auth(self, client: AsyncClient):
resp = await client.get("/api/v1/ldap-config")
assert resp.status_code == 401