sync: 同步 sql-platform-pro 最新代码 — 权限体系安全修复 v0.8.1

- P0: 实例访问控制全线接入权限组 (829测试)
- P1: permissions字段激活 + 操作级权限检查 (843测试)
- 前端: 删除数据权限页面, 权限组合并为唯一入口
- 新增: CodeMirror编辑器 + 查询模板 + 慢查询分析
- 新增: MongoDB管理页面 + Dashboard增强
- 修复: Pydantic V2迁移 + 前端TS错误修复
This commit is contained in:
logan committed 2026-05-23 14:41:06 +08:00
1 parent 6fcd9465cf
commit 4c9427e546
105 files changed
+23680 -471

No files matched your search

+250
View File
@@ -0,0 +1,250 @@
"""影响分析端点测试 — POST /api/v1/impact/analyze。"""
import pytest
from unittest.mock import patch, AsyncMock
from app.services.impact_analyzer import ImpactAnalysis
from tests.conftest import auth_header
# ───────────────────────── helper ─────────────────────────
def _make_analysis(**overrides) -> ImpactAnalysis:
"""构造一个默认的 ImpactAnalysis 对象,可按需覆盖字段。"""
defaults = dict(
sql_type="UPDATE",
estimated_rows=50,
estimated_duration_ms=200,
table_size_mb=32.5,
table_row_count=10000,
index_usage=["PRIMARY"],
has_full_scan=False,
risk_level="low",
warnings=[],
)
defaults.update(overrides)
return ImpactAnalysis(**defaults)
def _payload(instance_id: int, **overrides) -> dict:
"""构造分析请求体。"""
body = {
"instance_id": instance_id,
"database": "test_db",
"sql_content": "UPDATE users SET name='test' WHERE id=1;",
}
body.update(overrides)
return body
# ───────────────────────── 测试用例 ─────────────────────────
class TestImpactAnalyze:
"""影响分析端点基本测试。"""
@pytest.mark.asyncio
async def test_analyze_success(self, client, db, admin_token, test_instance):
"""正常分析 — 返回完整影响分析结果。"""
analysis = _make_analysis()
with patch(
"app.api.v1.endpoints.impact.ImpactAnalyzer.analyze",
new_callable=AsyncMock,
return_value=analysis,
):
resp = await client.post(
"/api/v1/impact/analyze",
json=_payload(test_instance.id),
headers=auth_header(admin_token),
)
assert resp.status_code == 200
data = resp.json()
# 验证所有字段都存在且类型正确
assert data["sql_type"] == "UPDATE"
assert data["estimated_rows"] == 50
assert data["estimated_duration_ms"] == 200
assert data["table_size_mb"] == 32.5
assert data["table_row_count"] == 10000
assert data["index_usage"] == ["PRIMARY"]
assert data["has_full_scan"] is False
assert data["risk_level"] == "low"
assert data["warnings"] == []
@pytest.mark.asyncio
async def test_analyze_instance_not_found(self, client, db, admin_token):
"""实例不存在 — 返回 404。"""
resp = await client.post(
"/api/v1/impact/analyze",
json=_payload(instance_id=99999),
headers=auth_header(admin_token),
)
assert resp.status_code == 404
assert "不存在" in resp.json()["detail"]
@pytest.mark.asyncio
async def test_analyze_requires_auth(self, client, db, test_instance):
"""未携带 Token — 返回 401。"""
resp = await client.post(
"/api/v1/impact/analyze",
json=_payload(test_instance.id),
)
assert resp.status_code == 401
@pytest.mark.asyncio
async def test_analyze_developer_allowed(self, client, db, developer_token, test_instance):
"""developer 角色也能调用(任何登录用户均可使用影响分析)。"""
analysis = _make_analysis(sql_type="SELECT")
with patch(
"app.api.v1.endpoints.impact.ImpactAnalyzer.analyze",
new_callable=AsyncMock,
return_value=analysis,
):
resp = await client.post(
"/api/v1/impact/analyze",
json=_payload(test_instance.id, sql_content="SELECT * FROM users LIMIT 10;"),
headers=auth_header(developer_token),
)
assert resp.status_code == 200
assert resp.json()["sql_type"] == "SELECT"
@pytest.mark.asyncio
async def test_analyze_missing_fields(self, client, db, admin_token, test_instance):
"""请求体缺少必填字段 — 返回 422 校验错误。"""
# 缺少 sql_content
resp = await client.post(
"/api/v1/impact/analyze",
json={"instance_id": test_instance.id, "database": "test_db"},
headers=auth_header(admin_token),
)
assert resp.status_code == 422
@pytest.mark.asyncio
async def test_analyze_high_risk_update(self, client, db, admin_token, test_instance):
"""高风险 UPDATE(全表扫描 + 大量行)— 返回 high/critical 风险等级。"""
analysis = _make_analysis(
sql_type="UPDATE",
estimated_rows=50000,
estimated_duration_ms=12000,
table_size_mb=200.0,
table_row_count=500000,
has_full_scan=True,
risk_level="high",
warnings=["UPDATE 缺少 WHERE 条件,将影响全表 500000 行", "可能触发全表扫描,建议添加索引条件"],
)
with patch(
"app.api.v1.endpoints.impact.ImpactAnalyzer.analyze",
new_callable=AsyncMock,
return_value=analysis,
):
resp = await client.post(
"/api/v1/impact/analyze",
json=_payload(
test_instance.id,
sql_content="UPDATE users SET status=0;",
),
headers=auth_header(admin_token),
)
assert resp.status_code == 200
data = resp.json()
assert data["risk_level"] == "high"
assert data["has_full_scan"] is True
assert len(data["warnings"]) > 0
@pytest.mark.asyncio
async def test_analyze_critical_drop(self, client, db, admin_token, test_instance):
"""DROP 语句 — 风险等级应为 critical。"""
analysis = _make_analysis(
sql_type="DROP",
risk_level="critical",
warnings=["DROP 操作不可逆,请确认已备份数据"],
)
with patch(
"app.api.v1.endpoints.impact.ImpactAnalyzer.analyze",
new_callable=AsyncMock,
return_value=analysis,
):
resp = await client.post(
"/api/v1/impact/analyze",
json=_payload(
test_instance.id,
sql_content="DROP TABLE users;",
),
headers=auth_header(admin_token),
)
assert resp.status_code == 200
data = resp.json()
assert data["risk_level"] == "critical"
assert any("不可逆" in w for w in data["warnings"])
@pytest.mark.asyncio
async def test_analyze_alter_large_table(self, client, db, admin_token, test_instance):
"""ALTER 大表 — 应标记为 critical 并建议使用在线变更工具。"""
analysis = _make_analysis(
sql_type="ALTER",
estimated_rows=2000000,
estimated_duration_ms=300000,
table_size_mb=2048.0,
table_row_count=2000000,
risk_level="critical",
warnings=[
"ALTER TABLE 操作表大小为 2048.0MB,建议使用 pt-online-schema-change 或 gh-ost 进行在线变更",
],
)
with patch(
"app.api.v1.endpoints.impact.ImpactAnalyzer.analyze",
new_callable=AsyncMock,
return_value=analysis,
):
resp = await client.post(
"/api/v1/impact/analyze",
json=_payload(
test_instance.id,
sql_content="ALTER TABLE big_table ADD COLUMN age INT;",
),
headers=auth_header(admin_token),
)
assert resp.status_code == 200
data = resp.json()
assert data["sql_type"] == "ALTER"
assert data["risk_level"] == "critical"
assert data["table_size_mb"] > 1024
assert any("pt-online-schema-change" in w or "gh-ost" in w for w in data["warnings"])
@pytest.mark.asyncio
async def test_analyze_response_contains_all_fields(self, client, db, admin_token, test_instance):
"""验证响应包含 ImpactAnalysisResponse 中定义的所有字段。"""
analysis = _make_analysis(
index_usage=["PRIMARY", "idx_name"],
warnings=["建议添加 LIMIT 条件"],
)
with patch(
"app.api.v1.endpoints.impact.ImpactAnalyzer.analyze",
new_callable=AsyncMock,
return_value=analysis,
):
resp = await client.post(
"/api/v1/impact/analyze",
json=_payload(test_instance.id),
headers=auth_header(admin_token),
)
assert resp.status_code == 200
data = resp.json()
expected_keys = {
"sql_type", "estimated_rows", "estimated_duration_ms",
"table_size_mb", "table_row_count", "index_usage",
"has_full_scan", "risk_level", "warnings",
}
assert set(data.keys()) == expected_keys
assert data["index_usage"] == ["PRIMARY", "idx_name"]
assert data["warnings"] == ["建议添加 LIMIT 条件"]