Files
opsxsql/tests/test_instance_detail.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

344 lines
12 KiB
Python

"""Tests for instance detail API endpoints and backup API."""
import sys
from unittest.mock import patch, AsyncMock, MagicMock
from contextlib import asynccontextmanager
import pytest
from httpx import AsyncClient
from tests.conftest import auth_header
def _make_aiomysql_mock():
"""Create a mock aiomysql module."""
aiomysql = MagicMock()
aiomysql.DictCursor = MagicMock()
return aiomysql
def _mock_mysql_connection(rows=None, description=None):
"""Create a mock MySQL connection that returns given rows."""
if rows is None:
rows = []
mock_cursor = AsyncMock()
mock_cursor.execute = AsyncMock()
mock_cursor.fetchall = AsyncMock(return_value=rows)
mock_cursor.description = description
@asynccontextmanager
async def mock_cursor_ctx(*args, **kwargs):
yield mock_cursor
mock_conn = AsyncMock()
mock_conn.cursor = mock_cursor_ctx
mock_conn.close = MagicMock()
return mock_conn, mock_cursor
class TestListDatabases:
async def test_list_databases_success(self, client: AsyncClient, test_instance, admin_token):
mock_conn, _ = _mock_mysql_connection([
{"Database": "test_db"},
{"Database": "myapp"},
{"Database": "information_schema"},
])
mock_aiomysql = _make_aiomysql_mock()
mock_aiomysql.connect = AsyncMock(return_value=mock_conn)
with patch.dict(sys.modules, {"aiomysql": mock_aiomysql}):
resp = await client.get(
"/api/v1/instances/" + str(test_instance.id) + "/detail/databases",
headers=auth_header(admin_token),
)
assert resp.status_code == 200
data = resp.json()
assert "test_db" in data
assert "myapp" in data
# information_schema should be filtered out
assert "information_schema" not in data
async def test_list_databases_requires_admin(self, client: AsyncClient, test_instance, developer_token):
resp = await client.get(
"/api/v1/instances/" + str(test_instance.id) + "/detail/databases",
headers=auth_header(developer_token),
)
assert resp.status_code == 403
class TestListTables:
async def test_list_tables_success(self, client: AsyncClient, test_instance, admin_token):
mock_conn, _ = _mock_mysql_connection([
{"Name": "users", "Engine": "InnoDB", "Rows": 100, "Data_length": 16384,
"Index_length": 0, "Comment": "", "Collation": "utf8mb4_general_ci",
"Create_time": "2024-01-01", "Update_time": None},
])
mock_aiomysql = _make_aiomysql_mock()
mock_aiomysql.connect = AsyncMock(return_value=mock_conn)
with patch.dict(sys.modules, {"aiomysql": mock_aiomysql}):
resp = await client.get(
"/api/v1/instances/" + str(test_instance.id) + "/detail/tables",
params={"database": "test_db"},
headers=auth_header(admin_token),
)
assert resp.status_code == 200
data = resp.json()
assert len(data) == 1
assert data[0]["name"] == "users"
assert data[0]["engine"] == "InnoDB"
assert data[0]["rows"] == 100
class TestTableDetail:
async def test_table_detail_success(self, client: AsyncClient, test_instance, admin_token):
# Need three separate connections for DDL, columns, indexes
ddl_conn, _ = _mock_mysql_connection([
{"Table": "users", "Create Table": "CREATE TABLE users (id INT PRIMARY KEY)"},
])
col_conn, _ = _mock_mysql_connection([
{"Field": "id", "Type": "int", "Null": "NO", "Key": "PRI", "Default": None, "Extra": "auto_increment"},
])
idx_conn, _ = _mock_mysql_connection([
{"Table": "users", "Non_unique": 0, "Key_name": "PRIMARY", "Column_name": "id"},
])
call_count = 0
async def mock_connect(*args, **kwargs):
nonlocal call_count
call_count += 1
if call_count == 1:
return ddl_conn
elif call_count == 2:
return col_conn
else:
return idx_conn
mock_aiomysql = _make_aiomysql_mock()
mock_aiomysql.connect = mock_connect
with patch.dict(sys.modules, {"aiomysql": mock_aiomysql}):
resp = await client.get(
"/api/v1/instances/" + str(test_instance.id) + "/detail/table-detail",
params={"database": "test_db", "table": "users"},
headers=auth_header(admin_token),
)
assert resp.status_code == 200
data = resp.json()
assert "ddl" in data
assert "columns" in data
assert "indexes" in data
assert "CREATE TABLE" in data["ddl"]
class TestListUsers:
async def test_list_users_success(self, client: AsyncClient, test_instance, admin_token):
mock_conn, _ = _mock_mysql_connection([
{"user": "root", "host": "localhost"},
{"user": "admin", "host": "%"},
])
mock_aiomysql = _make_aiomysql_mock()
mock_aiomysql.connect = AsyncMock(return_value=mock_conn)
with patch.dict(sys.modules, {"aiomysql": mock_aiomysql}):
resp = await client.get(
"/api/v1/instances/" + str(test_instance.id) + "/detail/users",
headers=auth_header(admin_token),
)
assert resp.status_code == 200
data = resp.json()
assert len(data) == 2
assert data[0]["user"] == "root"
class TestShowGrants:
async def test_show_grants_success(self, client: AsyncClient, test_instance, admin_token):
mock_conn, _ = _mock_mysql_connection([
{"Grants for root@localhost": "GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost'"},
])
mock_aiomysql = _make_aiomysql_mock()
mock_aiomysql.connect = AsyncMock(return_value=mock_conn)
with patch.dict(sys.modules, {"aiomysql": mock_aiomysql}):
resp = await client.get(
"/api/v1/instances/" + str(test_instance.id) + "/detail/grants",
params={"user": "root", "host": "localhost"},
headers=auth_header(admin_token),
)
assert resp.status_code == 200
data = resp.json()
assert data["user"] == "root"
assert len(data["grants"]) >= 1
class TestProcesslist:
async def test_show_processlist_success(self, client: AsyncClient, test_instance, admin_token):
mock_conn, _ = _mock_mysql_connection([
{"Id": 1, "User": "root", "Host": "localhost", "db": "test_db",
"Command": "Query", "Time": 10, "State": "executing", "Info": "SELECT 1"},
])
mock_aiomysql = _make_aiomysql_mock()
mock_aiomysql.connect = AsyncMock(return_value=mock_conn)
with patch.dict(sys.modules, {"aiomysql": mock_aiomysql}):
resp = await client.get(
"/api/v1/instances/" + str(test_instance.id) + "/detail/processlist",
headers=auth_header(admin_token),
)
assert resp.status_code == 200
data = resp.json()
assert len(data) == 1
assert data[0]["Id"] == 1
class TestKillProcess:
async def test_kill_process_success(self, client: AsyncClient, test_instance, admin_token):
mock_conn, _ = _mock_mysql_connection([])
mock_aiomysql = _make_aiomysql_mock()
mock_aiomysql.connect = AsyncMock(return_value=mock_conn)
with patch.dict(sys.modules, {"aiomysql": mock_aiomysql}):
resp = await client.post(
"/api/v1/instances/" + str(test_instance.id) + "/detail/processlist/123/kill",
headers=auth_header(admin_token),
)
assert resp.status_code == 200
data = resp.json()
assert data["pid"] == 123
assert "已终止" in data["message"]
class TestShowStatus:
async def test_show_status_success(self, client: AsyncClient, test_instance, admin_token):
mock_conn, _ = _mock_mysql_connection([
{"Variable_name": "Threads_connected", "Value": "5"},
{"Variable_name": "Queries", "Value": "1000"},
{"Variable_name": "Slow_queries", "Value": "10"},
{"Variable_name": "Uptime", "Value": "86400"},
])
mock_aiomysql = _make_aiomysql_mock()
mock_aiomysql.connect = AsyncMock(return_value=mock_conn)
with patch.dict(sys.modules, {"aiomysql": mock_aiomysql}):
resp = await client.get(
"/api/v1/instances/" + str(test_instance.id) + "/detail/status",
headers=auth_header(admin_token),
)
assert resp.status_code == 200
data = resp.json()
assert "key_metrics" in data
assert data["key_metrics"]["Threads_connected"] == "5"
class TestShowVariables:
async def test_show_variables_success(self, client: AsyncClient, test_instance, admin_token):
mock_conn, _ = _mock_mysql_connection([
{"Variable_name": "max_connections", "Value": "151"},
{"Variable_name": "slow_query_log", "Value": "ON"},
])
mock_aiomysql = _make_aiomysql_mock()
mock_aiomysql.connect = AsyncMock(return_value=mock_conn)
with patch.dict(sys.modules, {"aiomysql": mock_aiomysql}):
resp = await client.get(
"/api/v1/instances/" + str(test_instance.id) + "/detail/variables",
headers=auth_header(admin_token),
)
assert resp.status_code == 200
data = resp.json()
assert len(data) == 2
class TestBackupAPI:
async def test_create_backup(self, client: AsyncClient, test_instance, admin_token):
resp = await client.post(
"/api/v1/backups",
headers=auth_header(admin_token),
json={
"title": "测试备份",
"instance_id": test_instance.id,
"database": "test_db",
"remark": "自动备份",
},
)
assert resp.status_code == 201
data = resp.json()
assert data["title"] == "测试备份"
assert data["status"] == "pending"
assert data["database"] == "test_db"
async def test_list_backups(self, client: AsyncClient, test_instance, admin_token):
# Create a backup first
await client.post(
"/api/v1/backups",
headers=auth_header(admin_token),
json={
"title": "备份1",
"instance_id": test_instance.id,
"database": "test_db",
},
)
resp = await client.get(
"/api/v1/backups",
params={"instance_id": test_instance.id},
headers=auth_header(admin_token),
)
assert resp.status_code == 200
data = resp.json()
assert len(data) >= 1
async def test_get_backup_detail(self, client: AsyncClient, test_instance, admin_token):
# Create a backup
create_resp = await client.post(
"/api/v1/backups",
headers=auth_header(admin_token),
json={
"title": "备份详情测试",
"instance_id": test_instance.id,
"database": "test_db",
},
)
backup_id = create_resp.json()["id"]
resp = await client.get(
"/api/v1/backups/" + str(backup_id),
headers=auth_header(admin_token),
)
assert resp.status_code == 200
data = resp.json()
assert data["id"] == backup_id
assert data["title"] == "备份详情测试"
async def test_get_backup_not_found(self, client: AsyncClient, admin_token):
resp = await client.get(
"/api/v1/backups/99999",
headers=auth_header(admin_token),
)
assert resp.status_code == 404
async def test_backup_requires_admin(self, client: AsyncClient, developer_token):
resp = await client.get(
"/api/v1/backups",
headers=auth_header(developer_token),
)
assert resp.status_code == 403
class TestInstanceNotFound:
async def test_databases_instance_not_found(self, client: AsyncClient, admin_token):
resp = await client.get(
"/api/v1/instances/99999/detail/databases",
headers=auth_header(admin_token),
)
assert resp.status_code == 404