- P0: 实例访问控制全线接入权限组 (829测试) - P1: permissions字段激活 + 操作级权限检查 (843测试) - 前端: 删除数据权限页面, 权限组合并为唯一入口 - 新增: CodeMirror编辑器 + 查询模板 + 慢查询分析 - 新增: MongoDB管理页面 + Dashboard增强 - 修复: Pydantic V2迁移 + 前端TS错误修复
365 lines
14 KiB
Python
365 lines
14 KiB
Python
"""LDAP 配置管理端点测试 — 覆盖 GET/PUT /api/v1/ldap-config 和 POST /test"""
|
||
import pytest
|
||
from httpx import AsyncClient
|
||
|
||
from tests.conftest import auth_header
|
||
|
||
# ── 合法的 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"},
|
||
}
|
||
|
||
|
||
# ────────────────────────────────────────────────────────────────────
|
||
# 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",
|
||
"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"] == "******"
|
||
|
||
|
||
# ────────────────────────────────────────────────────────────────────
|
||
# 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 == 422
|
||
|
||
|
||
# ────────────────────────────────────────────────────────────────────
|
||
# 7. POST /test — 测试 LDAP 连接
|
||
# ────────────────────────────────────────────────────────────────────
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_ldap_connection_mock_success(client: AsyncClient, admin_token: str):
|
||
"""配置存在时 POST /test 在 mock 模式下应返回 success=True"""
|
||
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()
|
||
assert data["success"] is True
|
||
# mock 模式消息中包含 Mock 字样
|
||
assert "Mock" in data["message"]
|
||
# 消息中应包含主机和测试用户信息
|
||
assert "zhangsan" in data["message"]
|
||
|
||
|
||
@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
|