sync: 同步 sql-platform-pro 最新代码 — 权限体系安全修复 v0.8.1
- P0: 实例访问控制全线接入权限组 (829测试) - P1: permissions字段激活 + 操作级权限检查 (843测试) - 前端: 删除数据权限页面, 权限组合并为唯一入口 - 新增: CodeMirror编辑器 + 查询模板 + 慢查询分析 - 新增: MongoDB管理页面 + Dashboard增强 - 修复: Pydantic V2迁移 + 前端TS错误修复
This commit is contained in:
1 parent
6fcd9465cf
commit
4c9427e546
105 files changed
+23680
-471
No files matched your search
@@ -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 条件"]
|
||||
Reference in new issue
Block a user