Files
opsxsql/tests/test_ldap_config.py
T
logan 88e5f4e7ee feat: Go后端LDAP真实对接 — AES-256-GCM密码加解密 + 登录LDAP fallback
- 新增 crypto.go: AES-256-GCM 加解密,与 Python backend 兼容
- auth.go: Login 添加 LDAP fallback(本地优先→LDAP绑定→自动创建本地用户)
- ldap.go: 实现真实 LDAP 连接/用户搜索/密码验证/OU列表/用户列表
- 所有 LDAP 操作解密 admin_password 后使用
- 新增/更新密码存储时加密
- router.go: 补充 delete-user 路由
- 添加 go-ldap/v3 依赖
2026-05-25 23:32:15 +08:00

364 lines
14 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.
"""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 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