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:
logan committed 2026-05-25 23:32:15 +08:00
1 parent 622923821b
commit 88e5f4e7ee
71 files changed
+20390 -64

No files matched your search

+182
View File
@@ -0,0 +1,182 @@
"""回滚 SQL API 测试 — 覆盖 generate-rollback 和 preview 端点。"""
import pytest
from unittest.mock import patch, AsyncMock, MagicMock
from httpx import AsyncClient
from tests.conftest import auth_header
async def _create_ticket(client: AsyncClient, admin_token: str, instance_id: int) -> dict:
"""Helper: create a ticket and return the response body."""
payload = {
"title": "测试工单-回滚验证",
"instance_id": instance_id,
"target_database": "test_db",
"sql_content": "UPDATE users SET name='test' WHERE id=1",
"reason": "执行回滚测试用例",
"priority": "medium",
"db_type": "mysql",
}
resp = await client.post(
"/api/v1/tickets", json=payload, headers=auth_header(admin_token)
)
return resp
# ────────────────────────── Generate Rollback Tests ──────────────────────────
class TestGenerateRollback:
@pytest.mark.asyncio
async def test_generate_success(self, client, admin_token, test_instance):
"""生成回滚 SQL — mock RollbackGenerator 以避免真实连接。"""
ticket_resp = await _create_ticket(client, admin_token, test_instance.id)
assert ticket_resp.status_code == 201
ticket_id = ticket_resp.json()["id"]
with patch("app.api.v1.endpoints.rollback.RollbackGenerator") as MockGen:
instance_mock = MagicMock()
instance_mock.generate = AsyncMock(return_value="SELECT * FROM users WHERE id=1")
MockGen.return_value = instance_mock
resp = await client.post(
f"/api/v1/tickets/{ticket_id}/generate-rollback",
json={"auto_save": False},
headers=auth_header(admin_token),
)
assert resp.status_code == 200
body = resp.json()
assert body["ticket_id"] == ticket_id
assert "UPDATE" in body["original_sql"]
assert body["rollback_sql"] == "SELECT * FROM users WHERE id=1"
assert body["saved"] is False
@pytest.mark.asyncio
async def test_generate_with_auto_save(self, client, admin_token, test_instance):
"""auto_save=True 应保存回滚 SQL 到工单。"""
ticket_resp = await _create_ticket(client, admin_token, test_instance.id)
ticket_id = ticket_resp.json()["id"]
with patch("app.api.v1.endpoints.rollback.RollbackGenerator") as MockGen:
instance_mock = MagicMock()
instance_mock.generate = AsyncMock(return_value="DELETE FROM users WHERE id=1 AND name='test'")
MockGen.return_value = instance_mock
resp = await client.post(
f"/api/v1/tickets/{ticket_id}/generate-rollback",
json={"auto_save": True},
headers=auth_header(admin_token),
)
assert resp.status_code == 200
assert resp.json()["saved"] is True
@pytest.mark.asyncio
async def test_generate_ticket_not_found(self, client, admin_token):
resp = await client.post(
"/api/v1/tickets/9999/generate-rollback",
json={"auto_save": False},
headers=auth_header(admin_token),
)
assert resp.status_code == 404
@pytest.mark.asyncio
async def test_generate_developer_forbidden(self, client, developer_token, test_instance):
resp = await client.post(
"/api/v1/tickets/1/generate-rollback",
json={"auto_save": False},
headers=auth_header(developer_token),
)
assert resp.status_code == 403
@pytest.mark.asyncio
async def test_generate_guest_forbidden(self, client, guest_token):
resp = await client.post(
"/api/v1/tickets/1/generate-rollback",
json={"auto_save": False},
headers=auth_header(guest_token),
)
assert resp.status_code == 403
# ────────────────────────── Preview Rollback Tests ──────────────────────────
class TestPreviewRollback:
@pytest.mark.asyncio
async def test_preview_success(self, client, admin_token, test_instance):
with patch("app.api.v1.endpoints.rollback.RollbackGenerator") as MockGen:
instance_mock = MagicMock()
instance_mock.preview = AsyncMock(return_value="SELECT * FROM users WHERE id=1")
MockGen.return_value = instance_mock
resp = await client.post(
"/api/v1/rollback/preview",
json={
"instance_id": test_instance.id,
"database": "test_db",
"sql": "UPDATE users SET name='x' WHERE id=1",
},
headers=auth_header(admin_token),
)
assert resp.status_code == 200
body = resp.json()
assert "UPDATE" in body["original_sql"]
assert body["rollback_sql"] == "SELECT * FROM users WHERE id=1"
@pytest.mark.asyncio
async def test_preview_instance_not_found(self, client, admin_token):
with patch("app.api.v1.endpoints.rollback.RollbackGenerator") as MockGen:
MockGen.return_value = MagicMock()
resp = await client.post(
"/api/v1/rollback/preview",
json={
"instance_id": 9999,
"database": "test_db",
"sql": "DELETE FROM t WHERE id=1",
},
headers=auth_header(admin_token),
)
assert resp.status_code == 404
@pytest.mark.asyncio
async def test_preview_any_authenticated_user(self, client, developer_token, test_instance):
"""preview 允许所有认证用户 (get_current_user)。"""
with patch("app.api.v1.endpoints.rollback.RollbackGenerator") as MockGen:
instance_mock = MagicMock()
instance_mock.preview = AsyncMock(return_value="-- 回滚SQL预览")
MockGen.return_value = instance_mock
resp = await client.post(
"/api/v1/rollback/preview",
json={
"instance_id": test_instance.id,
"database": "test_db",
"sql": "UPDATE t SET a=1 WHERE id=1",
},
headers=auth_header(developer_token),
)
assert resp.status_code == 200
@pytest.mark.asyncio
async def test_preview_no_auth_401(self, client):
resp = await client.post(
"/api/v1/rollback/preview",
json={
"instance_id": 1,
"database": "test_db",
"sql": "UPDATE t SET a=1",
},
)
assert resp.status_code == 401
@pytest.mark.asyncio
async def test_preview_validation_error(self, client, admin_token):
"""缺少必填字段 → 422。"""
resp = await client.post(
"/api/v1/rollback/preview",
json={"instance_id": 1},
headers=auth_header(admin_token),
)
assert resp.status_code == 422