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 依赖
This commit is contained in:
1 parent
622923821b
commit
88e5f4e7ee
71 files changed
+20390
-64
No files matched your search
@@ -0,0 +1,329 @@
|
||||
"""连接池管理端点完整测试 — pool.py
|
||||
|
||||
覆盖端点:
|
||||
GET /api/v1/pools/instances/{instance_id}/stats 连接池状态
|
||||
POST /api/v1/pools/instances/{instance_id}/reset 重置连接池
|
||||
GET /api/v1/pools/overview 所有连接池概览
|
||||
"""
|
||||
import pytest
|
||||
from unittest.mock import patch, AsyncMock, MagicMock
|
||||
from httpx import AsyncClient
|
||||
|
||||
from tests.conftest import auth_header
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# GET /pools/instances/{instance_id}/stats — 连接池状态
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
class TestPoolStats:
|
||||
|
||||
async def test_stats_not_initialized(
|
||||
self, client: AsyncClient, admin_token, test_instance
|
||||
):
|
||||
"""连接池未初始化时返回 not_initialized。"""
|
||||
with patch("app.api.v1.endpoints.pool.get_pool_manager") as mock_pm:
|
||||
manager = MagicMock()
|
||||
manager.get_pool_stats.return_value = None
|
||||
mock_pm.return_value = manager
|
||||
|
||||
resp = await client.get(
|
||||
f"/api/v1/pools/instances/{test_instance.id}/stats",
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["status"] == "not_initialized"
|
||||
assert body["instance_id"] == test_instance.id
|
||||
|
||||
async def test_stats_with_active_pool(
|
||||
self, client: AsyncClient, admin_token, test_instance
|
||||
):
|
||||
"""已初始化的连接池返回统计数据。"""
|
||||
with patch("app.api.v1.endpoints.pool.get_pool_manager") as mock_pm:
|
||||
stats = MagicMock()
|
||||
stats.to_dict.return_value = {
|
||||
"instance_id": test_instance.id,
|
||||
"active": 3,
|
||||
"idle": 2,
|
||||
"max_size": 10,
|
||||
}
|
||||
manager = MagicMock()
|
||||
manager.get_pool_stats.return_value = stats
|
||||
mock_pm.return_value = manager
|
||||
|
||||
resp = await client.get(
|
||||
f"/api/v1/pools/instances/{test_instance.id}/stats",
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["active"] == 3
|
||||
assert body["idle"] == 2
|
||||
|
||||
async def test_stats_instance_not_found(
|
||||
self, client: AsyncClient, admin_token
|
||||
):
|
||||
"""数据源不存在返回 404。"""
|
||||
resp = await client.get(
|
||||
"/api/v1/pools/instances/9999/stats",
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
assert resp.status_code == 404
|
||||
|
||||
async def test_stats_dba_allowed(
|
||||
self, client: AsyncClient, dba_token, test_instance
|
||||
):
|
||||
"""DBA 角色可以查看连接池状态。"""
|
||||
with patch("app.api.v1.endpoints.pool.get_pool_manager") as mock_pm:
|
||||
stats = MagicMock()
|
||||
stats.to_dict.return_value = {"active": 0, "idle": 0}
|
||||
manager = MagicMock()
|
||||
manager.get_pool_stats.return_value = stats
|
||||
mock_pm.return_value = manager
|
||||
|
||||
resp = await client.get(
|
||||
f"/api/v1/pools/instances/{test_instance.id}/stats",
|
||||
headers=auth_header(dba_token),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
async def test_stats_developer_forbidden(
|
||||
self, client: AsyncClient, developer_token, test_instance
|
||||
):
|
||||
"""developer 角色无权查看连接池状态。"""
|
||||
resp = await client.get(
|
||||
f"/api/v1/pools/instances/{test_instance.id}/stats",
|
||||
headers=auth_header(developer_token),
|
||||
)
|
||||
assert resp.status_code == 403
|
||||
|
||||
async def test_stats_guest_forbidden(
|
||||
self, client: AsyncClient, guest_token, test_instance
|
||||
):
|
||||
"""guest 角色无权查看连接池状态。"""
|
||||
resp = await client.get(
|
||||
f"/api/v1/pools/instances/{test_instance.id}/stats",
|
||||
headers=auth_header(guest_token),
|
||||
)
|
||||
assert resp.status_code == 403
|
||||
|
||||
async def test_stats_unauthenticated(
|
||||
self, client: AsyncClient, test_instance
|
||||
):
|
||||
"""未认证请求返回 401。"""
|
||||
resp = await client.get(
|
||||
f"/api/v1/pools/instances/{test_instance.id}/stats",
|
||||
)
|
||||
assert resp.status_code == 401
|
||||
|
||||
async def test_stats_returns_not_initialized_message(
|
||||
self, client: AsyncClient, admin_token, test_instance
|
||||
):
|
||||
"""未初始化时返回友好提示信息。"""
|
||||
with patch("app.api.v1.endpoints.pool.get_pool_manager") as mock_pm:
|
||||
manager = MagicMock()
|
||||
manager.get_pool_stats.return_value = None
|
||||
mock_pm.return_value = manager
|
||||
|
||||
resp = await client.get(
|
||||
f"/api/v1/pools/instances/{test_instance.id}/stats",
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
body = resp.json()
|
||||
assert "message" in body
|
||||
assert "首次查询" in body["message"] or "初始化" in body["message"]
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# POST /pools/instances/{instance_id}/reset — 重置连接池
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
class TestPoolReset:
|
||||
|
||||
async def test_reset_success(
|
||||
self, client: AsyncClient, admin_token, test_instance
|
||||
):
|
||||
"""管理员重置连接池成功。"""
|
||||
with patch("app.api.v1.endpoints.pool.get_pool_manager") as mock_pm:
|
||||
manager = MagicMock()
|
||||
manager.reset_pool = AsyncMock()
|
||||
mock_pm.return_value = manager
|
||||
|
||||
resp = await client.post(
|
||||
f"/api/v1/pools/instances/{test_instance.id}/reset",
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert "已重置" in body["message"]
|
||||
assert body["instance_id"] == test_instance.id
|
||||
|
||||
async def test_reset_dba_allowed(
|
||||
self, client: AsyncClient, dba_token, test_instance
|
||||
):
|
||||
"""DBA 角色可以重置连接池。"""
|
||||
with patch("app.api.v1.endpoints.pool.get_pool_manager") as mock_pm:
|
||||
manager = MagicMock()
|
||||
manager.reset_pool = AsyncMock()
|
||||
mock_pm.return_value = manager
|
||||
|
||||
resp = await client.post(
|
||||
f"/api/v1/pools/instances/{test_instance.id}/reset",
|
||||
headers=auth_header(dba_token),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
async def test_reset_developer_forbidden(
|
||||
self, client: AsyncClient, developer_token, test_instance
|
||||
):
|
||||
"""developer 角色无权重置连接池。"""
|
||||
resp = await client.post(
|
||||
f"/api/v1/pools/instances/{test_instance.id}/reset",
|
||||
headers=auth_header(developer_token),
|
||||
)
|
||||
assert resp.status_code == 403
|
||||
|
||||
async def test_reset_guest_forbidden(
|
||||
self, client: AsyncClient, guest_token, test_instance
|
||||
):
|
||||
"""guest 角色无权重置连接池。"""
|
||||
resp = await client.post(
|
||||
f"/api/v1/pools/instances/{test_instance.id}/reset",
|
||||
headers=auth_header(guest_token),
|
||||
)
|
||||
assert resp.status_code == 403
|
||||
|
||||
async def test_reset_unauthenticated(
|
||||
self, client: AsyncClient, test_instance
|
||||
):
|
||||
"""未认证请求返回 401。"""
|
||||
resp = await client.post(
|
||||
f"/api/v1/pools/instances/{test_instance.id}/reset",
|
||||
)
|
||||
assert resp.status_code == 401
|
||||
|
||||
async def test_reset_calls_manager_reset_pool(
|
||||
self, client: AsyncClient, admin_token, test_instance
|
||||
):
|
||||
"""验证 reset_pool 被正确调用。"""
|
||||
with patch("app.api.v1.endpoints.pool.get_pool_manager") as mock_pm:
|
||||
manager = MagicMock()
|
||||
manager.reset_pool = AsyncMock()
|
||||
mock_pm.return_value = manager
|
||||
|
||||
await client.post(
|
||||
f"/api/v1/pools/instances/{test_instance.id}/reset",
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
manager.reset_pool.assert_awaited_once_with(test_instance.id)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# GET /pools/overview — 所有连接池概览
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
class TestPoolsOverview:
|
||||
|
||||
async def test_overview_empty(self, client: AsyncClient, admin_token):
|
||||
"""无活跃连接池时返回空概览。"""
|
||||
with patch("app.api.v1.endpoints.pool.get_pool_manager") as mock_pm:
|
||||
manager = MagicMock()
|
||||
manager.get_all_stats.return_value = {}
|
||||
mock_pm.return_value = manager
|
||||
|
||||
resp = await client.get(
|
||||
"/api/v1/pools/overview",
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["total_pools"] == 0
|
||||
assert body["pools"] == []
|
||||
|
||||
async def test_overview_with_pools(self, client: AsyncClient, admin_token):
|
||||
"""有连接池时返回正确概览。"""
|
||||
with patch("app.api.v1.endpoints.pool.get_pool_manager") as mock_pm:
|
||||
stats1 = MagicMock()
|
||||
stats1.to_dict.return_value = {"instance_id": 1, "active": 2}
|
||||
stats2 = MagicMock()
|
||||
stats2.to_dict.return_value = {"instance_id": 2, "active": 1}
|
||||
|
||||
manager = MagicMock()
|
||||
manager.get_all_stats.return_value = {1: stats1, 2: stats2}
|
||||
mock_pm.return_value = manager
|
||||
|
||||
resp = await client.get(
|
||||
"/api/v1/pools/overview",
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["total_pools"] == 2
|
||||
assert len(body["pools"]) == 2
|
||||
|
||||
async def test_overview_dba_allowed(self, client: AsyncClient, dba_token):
|
||||
"""DBA 角色可以查看连接池概览。"""
|
||||
with patch("app.api.v1.endpoints.pool.get_pool_manager") as mock_pm:
|
||||
manager = MagicMock()
|
||||
manager.get_all_stats.return_value = {}
|
||||
mock_pm.return_value = manager
|
||||
|
||||
resp = await client.get(
|
||||
"/api/v1/pools/overview",
|
||||
headers=auth_header(dba_token),
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
async def test_overview_developer_forbidden(
|
||||
self, client: AsyncClient, developer_token
|
||||
):
|
||||
"""developer 角色无权查看连接池概览。"""
|
||||
resp = await client.get(
|
||||
"/api/v1/pools/overview",
|
||||
headers=auth_header(developer_token),
|
||||
)
|
||||
assert resp.status_code == 403
|
||||
|
||||
async def test_overview_guest_forbidden(
|
||||
self, client: AsyncClient, guest_token
|
||||
):
|
||||
"""guest 角色无权查看连接池概览。"""
|
||||
resp = await client.get(
|
||||
"/api/v1/pools/overview",
|
||||
headers=auth_header(guest_token),
|
||||
)
|
||||
assert resp.status_code == 403
|
||||
|
||||
async def test_overview_unauthenticated(self, client: AsyncClient):
|
||||
"""未认证请求返回 401。"""
|
||||
resp = await client.get("/api/v1/pools/overview")
|
||||
assert resp.status_code == 401
|
||||
|
||||
async def test_overview_single_pool_data(
|
||||
self, client: AsyncClient, admin_token
|
||||
):
|
||||
"""单个连接池数据验证。"""
|
||||
with patch("app.api.v1.endpoints.pool.get_pool_manager") as mock_pm:
|
||||
stats = MagicMock()
|
||||
stats.to_dict.return_value = {
|
||||
"instance_id": 42,
|
||||
"active": 5,
|
||||
"idle": 3,
|
||||
"max_size": 10,
|
||||
}
|
||||
manager = MagicMock()
|
||||
manager.get_all_stats.return_value = {42: stats}
|
||||
mock_pm.return_value = manager
|
||||
|
||||
resp = await client.get(
|
||||
"/api/v1/pools/overview",
|
||||
headers=auth_header(admin_token),
|
||||
)
|
||||
body = resp.json()
|
||||
assert body["total_pools"] == 1
|
||||
assert body["pools"][0]["instance_id"] == 42
|
||||
assert body["pools"][0]["active"] == 5
|
||||
Reference in new issue
Block a user