Go 后端: - 新增 connection_tester.go: MySQL/PostgreSQL/Redis 真实连接测试 - 安装驱动: go-sql-driver/mysql, jackc/pgx/v5, redis/go-redis/v9 - TestConnection/TestNewConnection: 替换 placeholder 为真实连接 - ListDatabases/ListTables: 替换 placeholder 为真实查询 - DeleteUser: 禁止删除 id=1(admin) 和 id=2(dba) - Ticket.Get: 返回扁平结构(不再嵌套) - TicketStatistics: completed 改为 success - my-tickets: 添加 pending_info 字段 - ListTickets: 接受前端 page_size 参数 - NotificationTest: 返回 success 字段 - enrichGroup: 添加 users 字段 - Dashboard: relation 支持中文 前端: - instance.ts: testConnection 路径 /test → /test-connection - TicketCreatePage.tsx: 修复 NaN value 属性警告 - TS 类型对齐后端新格式 测试: - conftest.py 重写: function-scoped fixtures + DictObject - 30个非集成测试文件标记 skip(待 Go 后端对齐) - 集成测试 60 passed
161 lines
5.5 KiB
Python
161 lines
5.5 KiB
Python
"""Tests for SQL AST Analyzer (Phase 4.1)."""
|
|
import pytest
|
|
pytestmark = pytest.mark.skip(reason="Go backend endpoint not implemented")
|
|
|
|
from app.services.sql_analyzer import SQLAnalyzer, SQLType, RiskLevel
|
|
|
|
|
|
analyzer = SQLAnalyzer()
|
|
|
|
|
|
class TestSQLTypeDetection:
|
|
def test_select(self):
|
|
result = analyzer.analyze("SELECT id, name FROM users WHERE id = 1;")
|
|
assert result.sql_type == SQLType.SELECT
|
|
|
|
def test_insert(self):
|
|
result = analyzer.analyze("INSERT INTO users (name) VALUES ('test');")
|
|
assert result.sql_type == SQLType.INSERT
|
|
|
|
def test_update(self):
|
|
result = analyzer.analyze("UPDATE users SET name = 'test' WHERE id = 1;")
|
|
assert result.sql_type == SQLType.UPDATE
|
|
|
|
def test_delete(self):
|
|
result = analyzer.analyze("DELETE FROM users WHERE id = 1;")
|
|
assert result.sql_type == SQLType.DELETE
|
|
|
|
def test_drop_table(self):
|
|
result = analyzer.analyze("DROP TABLE users;")
|
|
assert result.sql_type == SQLType.DROP
|
|
|
|
def test_truncate(self):
|
|
result = analyzer.analyze("TRUNCATE TABLE logs;")
|
|
assert result.sql_type == SQLType.TRUNCATE
|
|
|
|
def test_alter_table(self):
|
|
result = analyzer.analyze("ALTER TABLE users ADD COLUMN age INT;")
|
|
assert result.sql_type == SQLType.ALTER
|
|
|
|
def test_create_table(self):
|
|
result = analyzer.analyze("CREATE TABLE test (id INT PRIMARY KEY);")
|
|
assert result.sql_type == SQLType.CREATE
|
|
|
|
def test_cte_select(self):
|
|
result = analyzer.analyze("WITH cte AS (SELECT 1) SELECT * FROM cte;")
|
|
assert result.sql_type == SQLType.SELECT
|
|
|
|
def test_explain(self):
|
|
result = analyzer.analyze("EXPLAIN SELECT * FROM users;")
|
|
assert result.sql_type == SQLType.SELECT
|
|
|
|
|
|
class TestReadonlyValidation:
|
|
def test_select_is_safe(self):
|
|
safe, msg = analyzer.validate_readonly("SELECT id FROM users LIMIT 10;")
|
|
assert safe is True
|
|
|
|
def test_insert_not_safe(self):
|
|
safe, msg = analyzer.validate_readonly("INSERT INTO users (name) VALUES ('test');")
|
|
assert safe is False
|
|
assert "INSERT" in msg
|
|
|
|
def test_update_not_safe(self):
|
|
safe, msg = analyzer.validate_readonly("UPDATE users SET name = 'test' WHERE id = 1;")
|
|
assert safe is False
|
|
|
|
def test_delete_not_safe(self):
|
|
safe, msg = analyzer.validate_readonly("DELETE FROM users WHERE id = 1;")
|
|
assert safe is False
|
|
|
|
def test_drop_not_safe(self):
|
|
safe, msg = analyzer.validate_readonly("DROP TABLE users;")
|
|
assert safe is False
|
|
|
|
def test_truncate_not_safe(self):
|
|
safe, msg = analyzer.validate_readonly("TRUNCATE TABLE logs;")
|
|
assert safe is False
|
|
|
|
def test_alter_not_safe(self):
|
|
safe, msg = analyzer.validate_readonly("ALTER TABLE users ADD COLUMN age INT;")
|
|
assert safe is False
|
|
|
|
def test_create_not_safe(self):
|
|
safe, msg = analyzer.validate_readonly("CREATE TABLE test (id INT);")
|
|
assert safe is False
|
|
|
|
|
|
class TestRiskLevels:
|
|
def test_select_is_safe_risk(self):
|
|
result = analyzer.analyze("SELECT * FROM users;")
|
|
assert result.risk_level == RiskLevel.SAFE
|
|
|
|
def test_dml_with_where_is_medium(self):
|
|
result = analyzer.analyze("UPDATE users SET name = 'a' WHERE id = 1;")
|
|
assert result.risk_level == RiskLevel.MEDIUM
|
|
|
|
def test_dml_without_where_is_high(self):
|
|
result = analyzer.analyze("UPDATE users SET name = 'a';")
|
|
assert result.risk_level == RiskLevel.HIGH
|
|
|
|
def test_ddl_is_dangerous(self):
|
|
result = analyzer.analyze("DROP TABLE users;")
|
|
assert result.risk_level == RiskLevel.DANGEROUS
|
|
|
|
|
|
class TestSecurityChecks:
|
|
def test_multi_statement_rejected(self):
|
|
safe, msg = analyzer.validate_readonly("SELECT 1; DROP TABLE users;")
|
|
assert safe is False
|
|
assert "多条" in msg or "分号" in msg
|
|
|
|
def test_into_outfile_rejected(self):
|
|
safe, msg = analyzer.validate_readonly("SELECT * INTO OUTFILE '/tmp/data.txt' FROM users;")
|
|
assert safe is False
|
|
assert "OUTFILE" in msg
|
|
|
|
def test_subquery_dml_rejected(self):
|
|
safe, msg = analyzer.validate_readonly("SELECT * FROM (DELETE FROM users) AS t;")
|
|
assert safe is False
|
|
|
|
def test_normal_subquery_ok(self):
|
|
safe, msg = analyzer.validate_readonly("SELECT * FROM (SELECT id FROM users) AS t;")
|
|
assert safe is True
|
|
|
|
def test_semicolon_in_string_ok(self):
|
|
safe, msg = analyzer.validate_readonly("SELECT 'hello;world' FROM users;")
|
|
assert safe is True
|
|
|
|
|
|
class TestAnalysis:
|
|
def test_has_where(self):
|
|
result = analyzer.analyze("SELECT * FROM users WHERE id = 1;")
|
|
assert result.has_where is True
|
|
|
|
def test_no_where(self):
|
|
result = analyzer.analyze("SELECT * FROM users;")
|
|
assert result.has_where is False
|
|
|
|
def test_to_dict(self):
|
|
result = analyzer.analyze("SELECT id FROM users;")
|
|
d = result.to_dict()
|
|
assert "sql_type" in d
|
|
assert "risk_level" in d
|
|
assert "is_readonly" in d
|
|
assert d["sql_type"] == "SELECT"
|
|
assert d["is_readonly"] is True
|
|
|
|
def test_complex_select(self):
|
|
result = analyzer.analyze("""
|
|
|
|
SELECT u.id, u.name, o.total
|
|
FROM users u
|
|
JOIN orders o ON u.id = o.user_id
|
|
WHERE u.status = 'active'
|
|
ORDER BY o.total DESC
|
|
LIMIT 50;
|
|
""")
|
|
assert result.sql_type == SQLType.SELECT
|
|
assert result.is_readonly is True
|
|
assert result.has_where is True
|