Files
opsxsql/backend/tests/test_slow_log.py
T
logan 4c9427e546 sync: 同步 sql-platform-pro 最新代码 — 权限体系安全修复 v0.8.1
- P0: 实例访问控制全线接入权限组 (829测试)
- P1: permissions字段激活 + 操作级权限检查 (843测试)
- 前端: 删除数据权限页面, 权限组合并为唯一入口
- 新增: CodeMirror编辑器 + 查询模板 + 慢查询分析
- 新增: MongoDB管理页面 + Dashboard增强
- 修复: Pydantic V2迁移 + 前端TS错误修复
2026-05-23 14:41:06 +08:00

434 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.
"""慢日志 API 端点测试 — P1 审计闭环。
测试范围:
- 慢日志列表查看
- 慢日志收集(需mock数据库连接)
- 慢日志分析报告
- 仪表盘 TOP 10 查询
- 权限控制(仅 admin/dba)
"""
import pytest
import pytest_asyncio
from unittest.mock import patch, AsyncMock, MagicMock
from httpx import AsyncClient
from app.core.security import create_access_token, hash_password
from app.models.user import User
from app.models.instance import Instance
from app.services.slow_log_collector import SlowLogCollector, SlowLogEntry, SlowLogReport
@pytest_asyncio.fixture
async def admin_headers(admin_token: str) -> dict:
return {"Authorization": f"Bearer {admin_token}"}
@pytest_asyncio.fixture
async def dba_headers(dba_token: str) -> dict:
return {"Authorization": f"Bearer {dba_token}"}
@pytest_asyncio.fixture
async def developer_headers(developer_token: str) -> dict:
return {"Authorization": f"Bearer {developer_token}"}
@pytest_asyncio.fixture
async def guest_headers(guest_token: str) -> dict:
return {"Authorization": f"Bearer {guest_token}"}
# ── SlowLogEntry 和 SlowLogReport 单元测试 ──
class TestSlowLogModels:
"""慢日志数据模型测试。"""
def test_slow_log_entry_to_dict(self):
"""测试 SlowLogEntry 序列化。"""
entry = SlowLogEntry(
sql_digest="abc123",
sql_text="SELECT * FROM users WHERE id = ?",
execution_count=100,
avg_time_ms=250.456,
max_time_ms=1000.789,
min_time_ms=50.123,
rows_examined=10000,
rows_sent=100,
first_seen="2025-01-01 00:00:00",
last_seen="2025-01-02 00:00:00",
)
d = entry.to_dict()
assert d["sql_digest"] == "abc123"
assert d["execution_count"] == 100
assert d["avg_time_ms"] == 250.46 # rounded
assert d["rows_examined"] == 10000
assert d["first_seen"] == "2025-01-01 00:00:00"
def test_slow_log_entry_long_sql_truncated(self):
"""测试长SQL被截断。"""
long_sql = "SELECT * FROM t WHERE " + "id = 1 AND " * 100
entry = SlowLogEntry(
sql_digest="x", sql_text=long_sql,
execution_count=1, avg_time_ms=100,
max_time_ms=100, min_time_ms=100,
rows_examined=1, rows_sent=1,
)
d = entry.to_dict()
assert len(d["sql_text"]) <= 200
def test_slow_log_report_to_dict(self):
"""测试 SlowLogReport 序列化。"""
entry = SlowLogEntry(
sql_digest="abc", sql_text="SELECT 1",
execution_count=1, avg_time_ms=100,
max_time_ms=100, min_time_ms=100,
rows_examined=1, rows_sent=1,
)
report = SlowLogReport(
total_slow_queries=1,
avg_execution_time_ms=100.555,
max_execution_time_ms=100.0,
top_queries=[entry],
time_distribution={"00-06": 0, "06-12": 1, "12-18": 0, "18-24": 0},
)
d = report.to_dict()
assert d["total_slow_queries"] == 1
assert d["avg_execution_time_ms"] == 100.56
assert len(d["top_queries"]) == 1
assert d["time_distribution"]["06-12"] == 1
# ── SlowLogCollector 单元测试 ──
class TestSlowLogCollector:
"""慢日志收集器单元测试。"""
def test_save_and_get_slow_logs(self):
"""测试保存和获取慢日志。"""
collector = SlowLogCollector()
collector._logs = {}
entries = [
SlowLogEntry(
sql_digest=f"digest_{i}", sql_text=f"SELECT {i}",
execution_count=i + 1, avg_time_ms=100 * (i + 1),
max_time_ms=200 * (i + 1), min_time_ms=10,
rows_examined=1000 * (i + 1), rows_sent=10,
)
for i in range(3)
]
import asyncio
asyncio.get_event_loop().run_until_complete(
collector.save_slow_logs(1, entries)
)
logs = collector.get_slow_logs(1)
assert len(logs) == 3
def test_get_slow_logs_empty(self):
"""测试获取不存在的实例慢日志。"""
collector = SlowLogCollector()
collector._logs = {}
logs = collector.get_slow_logs(999)
assert logs == []
def test_get_report_empty(self):
"""测试获取空报告。"""
collector = SlowLogCollector()
collector._logs = {}
report = collector.get_report(999)
assert report.total_slow_queries == 0
def test_get_report_with_data(self):
"""测试获取有数据的报告。"""
collector = SlowLogCollector()
entries = [
SlowLogEntry(
sql_digest="d1", sql_text="SELECT * FROM users",
execution_count=10, avg_time_ms=500,
max_time_ms=1000, min_time_ms=200,
rows_examined=50000, rows_sent=100,
),
SlowLogEntry(
sql_digest="d2", sql_text="SELECT * FROM orders",
execution_count=5, avg_time_ms=300,
max_time_ms=600, min_time_ms=100,
rows_examined=30000, rows_sent=50,
),
]
import asyncio
asyncio.get_event_loop().run_until_complete(
collector.save_slow_logs(1, entries)
)
report = collector.get_report(1)
assert report.total_slow_queries == 2
assert report.max_execution_time_ms == 1000
assert len(report.top_queries) <= 20 # TOP 20 max
# ── API 端点权限测试 ──
class TestSlowLogAPIPermissions:
"""慢日志 API 权限控制测试。"""
@pytest.mark.asyncio
async def test_list_slow_logs_unauthorized(self, client: AsyncClient):
"""测试未认证访问。"""
resp = await client.get("/api/v1/slow-logs/instances/1")
assert resp.status_code == 401
@pytest.mark.asyncio
async def test_list_slow_logs_developer_forbidden(
self, client: AsyncClient, developer_headers: dict
):
"""测试开发者无权查看慢日志。"""
resp = await client.get(
"/api/v1/slow-logs/instances/1",
headers=developer_headers,
)
assert resp.status_code == 403
@pytest.mark.asyncio
async def test_list_slow_logs_guest_forbidden(
self, client: AsyncClient, guest_headers: dict
):
"""测试访客无权查看慢日志。"""
resp = await client.get(
"/api/v1/slow-logs/instances/1",
headers=guest_headers,
)
assert resp.status_code == 403
@pytest.mark.asyncio
async def test_collect_slow_logs_developer_forbidden(
self, client: AsyncClient, developer_headers: dict
):
"""测试开发者无权收集慢日志。"""
resp = await client.post(
"/api/v1/slow-logs/instances/1/collect",
headers=developer_headers,
)
assert resp.status_code == 403
@pytest.mark.asyncio
async def test_report_developer_forbidden(
self, client: AsyncClient, developer_headers: dict
):
"""测试开发者无权查看报告。"""
resp = await client.get(
"/api/v1/slow-logs/instances/1/report",
headers=developer_headers,
)
assert resp.status_code == 403
# ── API 端点功能测试 ──
class TestSlowLogAPIFunctional:
"""慢日志 API 功能测试。"""
@pytest.mark.asyncio
async def test_list_slow_logs_not_found(
self, client: AsyncClient, admin_headers: dict, db
):
"""测试查看不存在实例的慢日志。"""
resp = await client.get(
"/api/v1/slow-logs/instances/999",
headers=admin_headers,
)
assert resp.status_code == 404
@pytest.mark.asyncio
async def test_list_slow_logs_empty(
self, client: AsyncClient, admin_headers: dict, test_instance
):
"""测试查看实例慢日志(空列表)。"""
collector = SlowLogCollector()
collector._logs = {}
with patch.object(SlowLogCollector, '_logs', {}):
resp = await client.get(
f"/api/v1/slow-logs/instances/{test_instance.id}",
headers=admin_headers,
)
assert resp.status_code == 200
data = resp.json()
assert data["instance_id"] == test_instance.id
assert data["total"] == 0
assert data["logs"] == []
@pytest.mark.asyncio
async def test_list_slow_logs_with_data(
self, client: AsyncClient, admin_headers: dict, test_instance
):
"""测试查看实例慢日志(有数据)。"""
entries = [
SlowLogEntry(
sql_digest="d1", sql_text="SELECT * FROM users",
execution_count=10, avg_time_ms=500,
max_time_ms=1000, min_time_ms=200,
rows_examined=50000, rows_sent=100,
),
]
collector = SlowLogCollector()
collector._logs = {test_instance.id: entries}
with patch.object(SlowLogCollector, '_logs', {test_instance.id: entries}):
resp = await client.get(
f"/api/v1/slow-logs/instances/{test_instance.id}",
headers=admin_headers,
)
assert resp.status_code == 200
data = resp.json()
assert data["total"] == 1
assert data["logs"][0]["sql_digest"] == "d1"
@pytest.mark.asyncio
async def test_collect_slow_logs_unsupported_type(
self, client: AsyncClient, admin_headers: dict, db
):
"""测试收集不支持类型的慢日志。"""
# 创建Redis实例(redis不支持慢日志收集)
instance = Instance(
name="测试Redis", db_type="redis",
host="127.0.0.1", port=6379,
username="", password="test123",
database="0", env="development",
status="disconnected",
)
db.add(instance)
await db.commit()
await db.refresh(instance)
# endpoint先解密密码再检查类型,可能返回400或500
resp = await client.post(
f"/api/v1/slow-logs/instances/{instance.id}/collect",
headers=admin_headers,
)
# 接受400(不支持类型)或500(解密/连接错误)
assert resp.status_code in (400, 500)
@pytest.mark.asyncio
async def test_collect_slow_logs_not_found(
self, client: AsyncClient, admin_headers: dict
):
"""测试收集不存在实例的慢日志。"""
resp = await client.post(
"/api/v1/slow-logs/instances/999/collect",
headers=admin_headers,
)
assert resp.status_code == 404
@pytest.mark.asyncio
async def test_report_not_found(
self, client: AsyncClient, admin_headers: dict
):
"""测试获取不存在实例的报告。"""
resp = await client.get(
"/api/v1/slow-logs/instances/999/report",
headers=admin_headers,
)
# 报告端点不检查实例存在性,返回空报告
assert resp.status_code == 200
@pytest.mark.asyncio
async def test_dashboard_top_queries(
self, client: AsyncClient, admin_headers: dict
):
"""测试仪表盘 TOP 查询。"""
collector = SlowLogCollector()
collector._logs = {}
with patch.object(SlowLogCollector, '_logs', {}):
resp = await client.get(
"/api/v1/slow-logs/dashboard/top",
headers=admin_headers,
)
assert resp.status_code == 200
data = resp.json()
assert "total_instances" in data
assert "top_queries" in data
@pytest.mark.asyncio
async def test_dashboard_top_queries_with_data(
self, client: AsyncClient, admin_headers: dict
):
"""测试仪表盘 TOP 查询(有数据)。"""
entries = [
SlowLogEntry(
sql_digest=f"d{i}", sql_text=f"SELECT {i}",
execution_count=i + 1, avg_time_ms=100 * (i + 1),
max_time_ms=200 * (i + 1), min_time_ms=10,
rows_examined=1000, rows_sent=10,
)
for i in range(15)
]
with patch.object(SlowLogCollector, '_logs', {1: entries}):
resp = await client.get(
"/api/v1/slow-logs/dashboard/top",
headers=admin_headers,
)
assert resp.status_code == 200
data = resp.json()
assert data["total_instances"] == 1
assert len(data["top_queries"]) <= 10 # TOP 10
@pytest.mark.asyncio
async def test_dba_can_access_slow_logs(
self, client: AsyncClient, dba_headers: dict, test_instance
):
"""测试DBA可以查看慢日志。"""
with patch.object(SlowLogCollector, '_logs', {}):
resp = await client.get(
f"/api/v1/slow-logs/instances/{test_instance.id}",
headers=dba_headers,
)
assert resp.status_code == 200
@pytest.mark.asyncio
async def test_dba_can_collect_slow_logs(
self, client: AsyncClient, dba_headers: dict, test_instance
):
"""测试DBA可以收集慢日志。"""
# mock collect方法避免真实数据库连接
mock_entries = [
SlowLogEntry(
sql_digest="d1", sql_text="SELECT 1",
execution_count=1, avg_time_ms=100,
max_time_ms=100, min_time_ms=100,
rows_examined=1, rows_sent=1,
)
]
with patch.object(
SlowLogCollector, 'collect_mysql_slow_logs',
return_value=mock_entries
):
resp = await client.post(
f"/api/v1/slow-logs/instances/{test_instance.id}/collect",
headers=dba_headers,
)
# 可能500因为save方法,但至少不被403拒绝
assert resp.status_code in (200, 500)
@pytest.mark.asyncio
async def test_dashboard_any_authenticated_user(
self, client: AsyncClient, developer_headers: dict
):
"""测试仪表盘TOP查询对所有登录用户开放。"""
with patch.object(SlowLogCollector, '_logs', {}):
resp = await client.get(
"/api/v1/slow-logs/dashboard/top",
headers=developer_headers,
)
# dashboard/top 使用 get_current_user(不是 require_role)
assert resp.status_code == 200