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:
1 parent
622923821b
commit
88e5f4e7ee
71 files changed
+20390
-64
No files matched your search
@@ -0,0 +1,326 @@
|
||||
"""SQL 助手服务测试 — 验证格式化、高亮、风险检测、EXPLAIN 解析和查询建议。"""
|
||||
|
||||
import pytest
|
||||
from app.services.sql_assistant import SQLAssistant
|
||||
|
||||
|
||||
# ===== SQL 格式化 =====
|
||||
|
||||
|
||||
class TestSQLFormat:
|
||||
"""SQL 格式化测试。"""
|
||||
|
||||
def test_format_simple_select(self):
|
||||
"""应格式化简单 SELECT。"""
|
||||
result = SQLAssistant.format_sql("select * from users where id = 1")
|
||||
assert result.is_valid is True
|
||||
assert "SELECT" in result.formatted
|
||||
assert "FROM" in result.formatted
|
||||
|
||||
def test_format_empty_sql(self):
|
||||
"""空 SQL 应返回无效。"""
|
||||
result = SQLAssistant.format_sql("")
|
||||
assert result.is_valid is False
|
||||
assert result.error is not None
|
||||
|
||||
def test_format_none_sql(self):
|
||||
"""None 应返回无效。"""
|
||||
result = SQLAssistant.format_sql(None)
|
||||
assert result.is_valid is False
|
||||
|
||||
def test_format_whitespace_sql(self):
|
||||
"""纯空格 SQL 应返回无效。"""
|
||||
result = SQLAssistant.format_sql(" ")
|
||||
assert result.is_valid is False
|
||||
|
||||
def test_format_preserves_content(self):
|
||||
"""格式化不应丢失 SQL 内容。"""
|
||||
sql = "SELECT id, name FROM users WHERE status = 'active' ORDER BY id LIMIT 10"
|
||||
result = SQLAssistant.format_sql(sql)
|
||||
assert result.is_valid is True
|
||||
assert "users" in result.formatted
|
||||
assert "status" in result.formatted
|
||||
|
||||
def test_format_complex_join(self):
|
||||
"""应格式化复杂 JOIN 查询。"""
|
||||
sql = "select u.id, u.name, o.total from users u join orders o on u.id = o.user_id where o.total > 100"
|
||||
result = SQLAssistant.format_sql(sql)
|
||||
assert result.is_valid is True
|
||||
assert "JOIN" in result.formatted.upper() or "join" in result.formatted
|
||||
|
||||
|
||||
# ===== SQL 高亮 =====
|
||||
|
||||
|
||||
class TestSQLHighlight:
|
||||
"""SQL 语法高亮和风险分析测试。"""
|
||||
|
||||
def test_highlight_select(self):
|
||||
"""应标记 SELECT 关键字。"""
|
||||
result = SQLAssistant.highlight_sql("SELECT * FROM users")
|
||||
assert "SELECT" in result.keywords
|
||||
|
||||
def test_highlight_extract_table(self):
|
||||
"""应从 FROM 子句提取表名。"""
|
||||
result = SQLAssistant.highlight_sql("SELECT * FROM users WHERE id = 1")
|
||||
assert "users" in result.tables
|
||||
|
||||
def test_highlight_extract_join_table(self):
|
||||
"""应从 JOIN 子句提取表名。"""
|
||||
result = SQLAssistant.highlight_sql("SELECT * FROM users u JOIN orders o ON u.id = o.user_id")
|
||||
assert "users" in result.tables
|
||||
assert "orders" in result.tables
|
||||
|
||||
def test_highlight_delete_risk(self):
|
||||
"""DELETE 应被标记为高风险。"""
|
||||
result = SQLAssistant.highlight_sql("DELETE FROM users WHERE id = 1")
|
||||
assert len(result.risks) > 0
|
||||
risk_ops = [r["operation"] for r in result.risks]
|
||||
assert "DELETE" in risk_ops
|
||||
|
||||
def test_highlight_update_risk(self):
|
||||
"""UPDATE 应被标记为中等风险。"""
|
||||
result = SQLAssistant.highlight_sql("UPDATE users SET name='x' WHERE id=1")
|
||||
assert len(result.risks) > 0
|
||||
risk_ops = [r["operation"] for r in result.risks]
|
||||
assert "UPDATE" in risk_ops
|
||||
|
||||
def test_highlight_drop_critical_risk(self):
|
||||
"""DROP 应被标记为关键风险。"""
|
||||
result = SQLAssistant.highlight_sql("DROP TABLE users")
|
||||
assert len(result.risks) > 0
|
||||
critical = [r for r in result.risks if r["level"] == "critical"]
|
||||
assert len(critical) > 0
|
||||
|
||||
def test_highlight_update_no_where_critical(self):
|
||||
"""缺少 WHERE 的 UPDATE 应被标记为关键风险。"""
|
||||
result = SQLAssistant.highlight_sql("UPDATE users SET name='x'")
|
||||
assert len(result.risks) > 0
|
||||
critical = [r for r in result.risks if r["level"] == "critical"]
|
||||
assert len(critical) > 0
|
||||
|
||||
def test_highlight_delete_no_where_critical(self):
|
||||
"""缺少 WHERE 的 DELETE 应被标记为关键风险。"""
|
||||
result = SQLAssistant.highlight_sql("DELETE FROM users")
|
||||
assert len(result.risks) > 0
|
||||
critical = [r for r in result.risks if r["level"] == "critical"]
|
||||
assert len(critical) > 0
|
||||
|
||||
def test_highlight_select_no_risk(self):
|
||||
"""SELECT 语句不应有风险标记。"""
|
||||
result = SQLAssistant.highlight_sql("SELECT id, name FROM users LIMIT 10")
|
||||
assert len(result.risks) == 0
|
||||
|
||||
def test_highlight_produces_html(self):
|
||||
"""高亮结果应包含 HTML 标签。"""
|
||||
result = SQLAssistant.highlight_sql("SELECT * FROM users")
|
||||
assert "sql-keyword" in result.highlighted
|
||||
|
||||
|
||||
# ===== EXPLAIN 解析 =====
|
||||
|
||||
|
||||
class TestExplainParse:
|
||||
"""MySQL EXPLAIN 结果解析测试。"""
|
||||
|
||||
def test_parse_empty_explain(self):
|
||||
"""空 EXPLAIN 应返回警告。"""
|
||||
result = SQLAssistant.parse_explain([])
|
||||
assert result.is_simple is True
|
||||
assert len(result.warnings) > 0
|
||||
|
||||
def test_parse_simple_index_scan(self):
|
||||
"""索引扫描的 EXPLAIN 应被正确解析。"""
|
||||
rows = [{
|
||||
"table": "users",
|
||||
"type": "ref",
|
||||
"key": "idx_status",
|
||||
"rows": 100,
|
||||
"Extra": "",
|
||||
}]
|
||||
result = SQLAssistant.parse_explain(rows)
|
||||
assert result.has_index is True
|
||||
assert result.has_full_scan is False
|
||||
assert result.estimated_rows == 100
|
||||
|
||||
def test_parse_full_table_scan(self):
|
||||
"""全表扫描的 EXPLAIN 应标记警告。"""
|
||||
rows = [{
|
||||
"table": "orders",
|
||||
"type": "ALL",
|
||||
"key": None,
|
||||
"rows": 500000,
|
||||
"Extra": "",
|
||||
}]
|
||||
result = SQLAssistant.parse_explain(rows)
|
||||
assert result.has_full_scan is True
|
||||
assert result.has_index is False
|
||||
assert result.estimated_rows == 500000
|
||||
assert len(result.warnings) > 0
|
||||
|
||||
def test_parse_filesort_warning(self):
|
||||
"""Using filesort 应产生警告。"""
|
||||
rows = [{
|
||||
"table": "users",
|
||||
"type": "ref",
|
||||
"key": "idx_status",
|
||||
"rows": 100,
|
||||
"Extra": "Using filesort",
|
||||
}]
|
||||
result = SQLAssistant.parse_explain(rows)
|
||||
filesort_warnings = [w for w in result.warnings if "filesort" in w.lower() or "排序" in w]
|
||||
assert len(filesort_warnings) > 0
|
||||
|
||||
def test_parse_temporary_table_warning(self):
|
||||
"""Using temporary 应产生警告。"""
|
||||
rows = [{
|
||||
"table": "users",
|
||||
"type": "ref",
|
||||
"key": "idx_status",
|
||||
"rows": 100,
|
||||
"Extra": "Using temporary",
|
||||
}]
|
||||
result = SQLAssistant.parse_explain(rows)
|
||||
temp_warnings = [w for w in result.warnings if "临时表" in w or "temporary" in w.lower()]
|
||||
assert len(temp_warnings) > 0
|
||||
|
||||
def test_parse_large_rows_warning(self):
|
||||
"""预估行数过大应产生警告。"""
|
||||
rows = [{
|
||||
"table": "big_table",
|
||||
"type": "ALL",
|
||||
"key": None,
|
||||
"rows": 500000,
|
||||
"Extra": "",
|
||||
}]
|
||||
result = SQLAssistant.parse_explain(rows)
|
||||
large_warnings = [w for w in result.warnings if "较大" in w or "较慢" in w]
|
||||
assert len(large_warnings) > 0
|
||||
|
||||
def test_parse_multi_table_join(self):
|
||||
"""多表 JOIN 应返回非简单查询。"""
|
||||
rows = [
|
||||
{"table": "users", "type": "ref", "key": "PRIMARY", "rows": 1, "Extra": ""},
|
||||
{"table": "orders", "type": "ref", "key": "idx_user", "rows": 10, "Extra": ""},
|
||||
]
|
||||
result = SQLAssistant.parse_explain(rows)
|
||||
assert result.is_simple is False
|
||||
assert result.has_index is True
|
||||
assert result.estimated_rows == 11
|
||||
|
||||
def test_parse_produces_human_readable(self):
|
||||
"""应生成人类可读的执行计划描述。"""
|
||||
rows = [{
|
||||
"table": "users",
|
||||
"type": "ref",
|
||||
"key": "idx_name",
|
||||
"rows": 50,
|
||||
"Extra": "",
|
||||
}]
|
||||
result = SQLAssistant.parse_explain(rows)
|
||||
assert result.human_readable != ""
|
||||
assert "users" in result.human_readable
|
||||
|
||||
def test_parse_case_insensitive_keys(self):
|
||||
"""应处理大写的 EXPLAIN 键名。"""
|
||||
rows = [{
|
||||
"TABLE": "users",
|
||||
"TYPE": "ALL",
|
||||
"KEY": None,
|
||||
"ROWS": 1000,
|
||||
"EXTRA": "",
|
||||
}]
|
||||
result = SQLAssistant.parse_explain(rows)
|
||||
assert result.estimated_rows == 1000
|
||||
|
||||
|
||||
# ===== 查询建议 =====
|
||||
|
||||
|
||||
class TestQuerySuggestions:
|
||||
"""SQL 查询建议测试。"""
|
||||
|
||||
def test_select_star_suggestion(self):
|
||||
"""SELECT * 应触发优化建议。"""
|
||||
suggestions = SQLAssistant.get_query_suggestions("SELECT * FROM users")
|
||||
types = [s["type"] for s in suggestions]
|
||||
assert "optimization" in types
|
||||
|
||||
def test_no_limit_suggestion(self):
|
||||
"""缺少 LIMIT 应触发安全建议。"""
|
||||
suggestions = SQLAssistant.get_query_suggestions("SELECT id, name FROM users WHERE status = 'active'")
|
||||
msgs = [s["message"] for s in suggestions]
|
||||
assert any("LIMIT" in m for m in msgs)
|
||||
|
||||
def test_order_by_rand_suggestion(self):
|
||||
"""ORDER BY RAND() 应触发优化建议。"""
|
||||
suggestions = SQLAssistant.get_query_suggestions("SELECT * FROM users ORDER BY RAND() LIMIT 5")
|
||||
msgs = [s["message"] for s in suggestions]
|
||||
assert any("RAND" in m for m in msgs)
|
||||
|
||||
def test_update_no_where_suggestion(self):
|
||||
"""缺少 WHERE 的 UPDATE 应触发危险建议。"""
|
||||
suggestions = SQLAssistant.get_query_suggestions("UPDATE users SET name='x'")
|
||||
danger = [s for s in suggestions if s["type"] == "danger"]
|
||||
assert len(danger) > 0
|
||||
|
||||
def test_delete_no_where_suggestion(self):
|
||||
"""缺少 WHERE 的 DELETE 应触发危险建议。"""
|
||||
suggestions = SQLAssistant.get_query_suggestions("DELETE FROM users")
|
||||
danger = [s for s in suggestions if s["type"] == "danger"]
|
||||
assert len(danger) > 0
|
||||
|
||||
def test_complex_query_comment_suggestion(self):
|
||||
"""无注释的复杂查询应触发风格建议。"""
|
||||
long_sql = "SELECT * FROM users WHERE " + " AND ".join(["column_name_" + str(i) + " = 'value_number_" + str(i) + "'" for i in range(30)])
|
||||
suggestions = SQLAssistant.get_query_suggestions(long_sql)
|
||||
style = [s for s in suggestions if s["type"] == "style"]
|
||||
assert len(style) > 0
|
||||
|
||||
def test_many_joins_suggestion(self):
|
||||
"""多个 JOIN 应触发优化建议。"""
|
||||
sql = "SELECT * FROM a JOIN b ON a.id = b.aid JOIN c ON b.id = c.bid JOIN d ON c.id = d.cid JOIN e ON d.id = e.did"
|
||||
suggestions = SQLAssistant.get_query_suggestions(sql)
|
||||
join_suggestions = [s for s in suggestions if "JOIN" in s["message"]]
|
||||
assert len(join_suggestions) > 0
|
||||
|
||||
def test_clean_select_no_suggestions(self):
|
||||
"""干净的 SELECT 查询不应有危险建议。"""
|
||||
suggestions = SQLAssistant.get_query_suggestions("SELECT id, name FROM users WHERE status = 'active' LIMIT 10")
|
||||
danger = [s for s in suggestions if s["type"] == "danger"]
|
||||
assert len(danger) == 0
|
||||
|
||||
|
||||
# ===== 统一分析入口 =====
|
||||
|
||||
|
||||
class TestAnalyze:
|
||||
"""SQLAssistant.analyze 统一入口测试。"""
|
||||
|
||||
def test_analyze_returns_all_fields(self):
|
||||
"""应返回所有分析字段。"""
|
||||
result = SQLAssistant.analyze("SELECT * FROM users LIMIT 10")
|
||||
assert "formatted" in result
|
||||
assert "is_valid" in result
|
||||
assert "highlighted" in result
|
||||
assert "keywords" in result
|
||||
assert "tables" in result
|
||||
assert "risks" in result
|
||||
assert "suggestions" in result
|
||||
assert "risk_count" in result
|
||||
assert "suggestion_count" in result
|
||||
|
||||
def test_analyze_select_star(self):
|
||||
"""SELECT * 分析应有优化建议。"""
|
||||
result = SQLAssistant.analyze("SELECT * FROM users")
|
||||
assert result["suggestion_count"] > 0
|
||||
|
||||
def test_analyze_dangerous_update(self):
|
||||
"""危险 UPDATE 分析应有风险标记和建议。"""
|
||||
result = SQLAssistant.analyze("UPDATE users SET name='x'")
|
||||
assert result["risk_count"] > 0
|
||||
|
||||
def test_analyze_with_db_type(self):
|
||||
"""应支持指定数据库类型。"""
|
||||
result = SQLAssistant.analyze("SELECT * FROM users", db_type="postgres")
|
||||
assert result["is_valid"] is True
|
||||
Reference in new issue
Block a user