Files
opsxsql/backend/tests/test_phase1_features.py
T
logan 7be0bf1a7c feat: Phase1安全基线 — 风险等级+审核超时+执行进度+通知脱敏+高危确认+维护窗口 (1119测试)
新增功能:
- ticket_sql_items模型: 逐条SQL执行记录(状态/耗时/影响行数)
- risk_evaluator.py: 4级风险评估(low/medium/high/critical), 支持MySQL/PG/Redis/Mongo
- Ticket模型: 新增risk_level + workflow_id字段
- 审核引擎签名改造: audit(sql, db_type, instance_id=None) 支持EXPLAIN预检
- 审核超时保护: 10s超时跳过审核, 防止工单创建阻塞
- 工单创建自动风险计算 + 审批流匹配(instance.workflow_id优先)
- SQL items查询API: GET /{ticket_id}/sql-items 含进度统计
- 执行器集成: 执行完成后自动更新SQL items逐条状态
- 通知脱敏: password/secret/token自动掩码, SQL预览截断100字符
- 高危操作强制二次确认: high/critical级别需confirmed=true
- 维护窗口控制: system_settings配置执行时间窗口(支持跨午夜)
- 45个新测试: 风险评估24+通知脱敏5+维护窗口2+SQL分类11+配置2
2026-05-25 10:25:42 +08:00

281 lines
11 KiB
Python

"""风险等级评估 + 维护窗口 + 通知脱敏 + SQL items 测试。"""
import json
import pytest
from unittest.mock import patch, MagicMock
# ===== 风险等级评估测试 =====
class TestRiskEvaluator:
"""风险等级自动计算测试。"""
def test_select_is_low(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("SELECT * FROM users WHERE id = 1") == "low"
def test_show_is_low(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("SHOW TABLES") == "low"
def test_explain_is_low(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("EXPLAIN SELECT * FROM users") == "low"
def test_create_table_is_medium(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("CREATE TABLE t1 (id INT)") == "medium"
def test_alter_table_is_medium(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("ALTER TABLE users ADD COLUMN age INT") == "medium"
def test_insert_with_where_is_medium(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level(
"INSERT INTO users (name) VALUES ('test')",
sql_type="DML",
) == "medium"
def test_update_without_where_is_high(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("UPDATE users SET status = 1") == "high"
def test_delete_without_where_is_high(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("DELETE FROM users") == "high"
def test_alter_drop_column_is_high(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("ALTER TABLE users DROP COLUMN age") == "high"
def test_alter_modify_column_is_high(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("ALTER TABLE users MODIFY COLUMN age BIGINT") == "high"
def test_drop_table_is_critical(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("DROP TABLE users") == "critical"
def test_truncate_is_critical(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("TRUNCATE TABLE users") == "critical"
def test_drop_database_is_critical(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("DROP DATABASE mydb") == "critical"
def test_many_statements_is_high(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level(
"SELECT 1", statement_count=10
) == "high"
def test_multiple_audit_errors_is_high(self):
from app.services.risk_evaluator import evaluate_risk_level
findings = [
{"level": "error", "rule_id": "R1"},
{"level": "error", "rule_id": "R2"},
]
assert evaluate_risk_level(
"UPDATE users SET x=1 WHERE id=1",
sql_type="DML",
audit_findings=findings,
) == "high"
def test_delete_with_where_no_limit_is_medium(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level(
"DELETE FROM users WHERE id > 100"
) == "medium"
def test_delete_with_where_and_limit_is_low(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level(
"DELETE FROM users WHERE id > 100 LIMIT 10"
) == "low"
# Redis 风险
def test_redis_flushall_is_critical(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("FLUSHALL", db_type="redis") == "critical"
def test_redis_flushdb_is_critical(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("FLUSHDB", db_type="redis") == "critical"
def test_redis_set_is_medium(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("SET key value", db_type="redis") == "medium"
def test_redis_get_is_low(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("GET key", db_type="redis") == "low"
# MongoDB 风险
def test_mongo_drop_database_is_critical(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("db.dropDatabase()", db_type="mongodb") == "critical"
def test_mongo_drop_database_upper_is_critical(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("DROP DATABASE mydb", db_type="mongodb") == "critical"
def test_mongo_insert_is_medium(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("db.users.insertOne({name: 'test'})", db_type="mongodb") == "medium"
def test_mongo_find_is_low(self):
from app.services.risk_evaluator import evaluate_risk_level
assert evaluate_risk_level("db.users.find({})", db_type="mongodb") == "low"
# ===== 风险等级配置测试 =====
class TestRiskLevelConfig:
"""风险等级配置验证。"""
def test_all_levels_have_config(self):
from app.services.risk_evaluator import RISK_LEVEL_CONFIG
for level in ["low", "medium", "high", "critical"]:
assert level in RISK_LEVEL_CONFIG
cfg = RISK_LEVEL_CONFIG[level]
assert "label" in cfg
assert "color" in cfg
assert "min_approvers" in cfg
def test_high_and_critical_require_2_approvers(self):
from app.services.risk_evaluator import RISK_LEVEL_CONFIG
assert RISK_LEVEL_CONFIG["high"]["min_approvers"] >= 2
assert RISK_LEVEL_CONFIG["critical"]["min_approvers"] >= 2
# ===== 通知脱敏测试 =====
class TestNotificationMasking:
"""通知内容脱敏测试。"""
def test_mask_password_in_sql_preview(self):
from app.services.notification_sender import NotificationSender
data = {"sql_preview": "SET password='secret123' WHERE id=1"}
masked = NotificationSender._mask_sensitive_data(data)
assert "secret123" not in masked["sql_preview"]
assert "***" in masked["sql_preview"]
def test_mask_drop_table_in_sql_preview(self):
from app.services.notification_sender import NotificationSender
data = {"sql_preview": "DROP TABLE users"}
masked = NotificationSender._mask_sensitive_data(data)
assert "***" in masked["sql_preview"]
def test_mask_error_field(self):
from app.services.notification_sender import NotificationSender
data = {"error": "Connection failed: password='mysecret'"}
masked = NotificationSender._mask_sensitive_data(data)
assert "mysecret" not in masked["error"]
def test_truncate_long_sql_preview(self):
from app.services.notification_sender import NotificationSender
long_sql = "SELECT * FROM users WHERE " + "x=1 AND " * 50
data = {"sql_preview": long_sql}
masked = NotificationSender._mask_sensitive_data(data)
assert len(masked["sql_preview"]) <= 110 # 100 + "..."
def test_non_sensitive_data_unchanged(self):
from app.services.notification_sender import NotificationSender
data = {"title": "测试工单", "applicant": "张三"}
masked = NotificationSender._mask_sensitive_data(data)
assert masked["title"] == "测试工单"
assert masked["applicant"] == "张三"
# ===== 维护窗口测试 =====
class TestMaintenanceWindow:
"""维护窗口逻辑测试。"""
def test_no_config_allows_execution(self):
"""无配置时允许执行。"""
from app.api.v1.endpoints.ticket import _check_maintenance_window
from unittest.mock import AsyncMock
import asyncio
mock_db = AsyncMock()
mock_result = MagicMock()
mock_result.scalar_one_or_none.return_value = None
mock_db.execute.return_value = mock_result
async def run():
result = await _check_maintenance_window(mock_db)
assert result is None
asyncio.get_event_loop().run_until_complete(run())
def test_disabled_window_allows_execution(self):
"""disabled 的窗口允许执行。"""
from app.api.v1.endpoints.ticket import _check_maintenance_window
from app.models.system_setting import SystemSetting
from unittest.mock import AsyncMock
import asyncio
setting = SystemSetting(key="maintenance_window", value='{"enabled": false}')
mock_db = AsyncMock()
mock_result = MagicMock()
mock_result.scalar_one_or_none.return_value = setting
mock_db.execute.return_value = mock_result
async def run():
result = await _check_maintenance_window(mock_db)
assert result is None
asyncio.get_event_loop().run_until_complete(run())
# ===== SQL 类型分类测试 =====
class TestClassifySqlType:
"""SQL 类型分类辅助函数测试。"""
def test_select_is_dql(self):
from app.api.v1.endpoints.ticket import _classify_sql_type
assert _classify_sql_type("SELECT * FROM t") == "DQL"
def test_insert_is_dml(self):
from app.api.v1.endpoints.ticket import _classify_sql_type
assert _classify_sql_type("INSERT INTO t VALUES (1)") == "DML"
def test_update_is_dml(self):
from app.api.v1.endpoints.ticket import _classify_sql_type
assert _classify_sql_type("UPDATE t SET x=1") == "DML"
def test_delete_is_dml(self):
from app.api.v1.endpoints.ticket import _classify_sql_type
assert _classify_sql_type("DELETE FROM t") == "DML"
def test_create_is_ddl(self):
from app.api.v1.endpoints.ticket import _classify_sql_type
assert _classify_sql_type("CREATE TABLE t (id INT)") == "DDL"
def test_alter_is_ddl(self):
from app.api.v1.endpoints.ticket import _classify_sql_type
assert _classify_sql_type("ALTER TABLE t ADD COLUMN x INT") == "DDL"
def test_drop_is_ddl(self):
from app.api.v1.endpoints.ticket import _classify_sql_type
assert _classify_sql_type("DROP TABLE t") == "DDL"
def test_grant_is_dcl(self):
from app.api.v1.endpoints.ticket import _classify_sql_type
assert _classify_sql_type("GRANT SELECT ON t TO user") == "DCL"
def test_redis_set(self):
from app.api.v1.endpoints.ticket import _classify_sql_type
assert _classify_sql_type("SET key value") == "SET"
def test_redis_get(self):
from app.api.v1.endpoints.ticket import _classify_sql_type
assert _classify_sql_type("GET key") == "SET"
def test_unknown(self):
from app.api.v1.endpoints.ticket import _classify_sql_type
assert _classify_sql_type("UNKNOWN COMMAND") == "OTHER"