"""Tests for SQL Audit Engine (Phase 3.2).""" import pytest from app.services.audit_engine import SQLAuditEngine, Severity class TestSQLAuditEngine: def setup_method(self): self.engine = SQLAuditEngine(env="development") # --- DDL Rules --- def test_drop_table_without_if_exists(self): report = self.engine.audit("DROP TABLE users;") assert not report.passed or report.has_warnings rules = [f.rule for f in report.findings] assert "DDL_IF_EXISTS" in rules assert "DROP_TABLE" in rules def test_drop_table_production_blocked(self): engine = SQLAuditEngine(env="production") report = engine.audit("DROP TABLE users;") assert not report.passed errors = [f for f in report.findings if f.level == Severity.ERROR] assert any(f.rule == "DROP_TABLE_PRODUCTION" for f in errors) def test_truncate_warning(self): report = self.engine.audit("TRUNCATE TABLE logs;") rules = [f.rule for f in report.findings] assert "TRUNCATE" in rules def test_truncate_production_error(self): engine = SQLAuditEngine(env="production") report = engine.audit("TRUNCATE TABLE logs;") errors = [f for f in report.findings if f.level == Severity.ERROR] assert any(f.rule == "TRUNCATE" for f in errors) # --- Missing WHERE --- def test_update_no_where_error(self): report = self.engine.audit("UPDATE users SET status = 1;") assert not report.passed errors = [f.rule for f in report.findings if f.level == Severity.ERROR] assert "UPDATE_NO_WHERE" in errors def test_delete_no_where_error(self): report = self.engine.audit("DELETE FROM users;") assert not report.passed errors = [f.rule for f in report.findings if f.level == Severity.ERROR] assert "DELETE_NO_WHERE" in errors def test_update_with_where_passes(self): report = self.engine.audit("UPDATE users SET name = 'test' WHERE id = 1;") error_rules = [f.rule for f in report.findings if f.level == Severity.ERROR] assert "UPDATE_NO_WHERE" not in error_rules def test_delete_with_where_passes(self): report = self.engine.audit("DELETE FROM users WHERE id = 1;") error_rules = [f.rule for f in report.findings if f.level == Severity.ERROR] assert "DELETE_NO_WHERE" not in error_rules # --- SELECT * --- def test_select_star_info(self): report = self.engine.audit("SELECT * FROM users WHERE id = 1;") rules = [f.rule for f in report.findings] assert "SELECT_STAR" in rules star_finding = next(f for f in report.findings if f.rule == "SELECT_STAR") assert star_finding.level == Severity.INFO def test_select_columns_clean(self): report = self.engine.audit("SELECT id, name FROM users WHERE id = 1;") rules = [f.rule for f in report.findings] assert "SELECT_STAR" not in rules # --- Multiple statements --- def test_multi_statement_error(self): report = self.engine.audit("UPDATE users SET name = 'a'; DELETE FROM logs;") assert not report.passed rules = [f.rule for f in report.findings if f.level == Severity.ERROR] assert "MULTI_STATEMENT" in rules def test_single_statement_ok(self): report = self.engine.audit("SELECT id FROM users LIMIT 10;") rules = [f.rule for f in report.findings] assert "MULTI_STATEMENT" not in rules # --- Large IN clause --- def test_large_in_warning(self): values = ", ".join([str(i) for i in range(150)]) sql = f"SELECT * FROM users WHERE id IN ({values});" report = self.engine.audit(sql) rules = [f.rule for f in report.findings] assert "LARGE_IN" in rules def test_small_in_ok(self): report = self.engine.audit("SELECT * FROM users WHERE id IN (1, 2, 3);") rules = [f.rule for f in report.findings] assert "LARGE_IN" not in rules # --- Production extra rules --- def test_alter_table_production_error(self): engine = SQLAuditEngine(env="production") report = engine.audit("ALTER TABLE users ADD COLUMN age INT;") assert not report.passed errors = [f.rule for f in report.findings if f.level == Severity.ERROR] assert "ALTER_TABLE_PRODUCTION" in errors def test_alter_table_dev_ok(self): report = self.engine.audit("ALTER TABLE users ADD COLUMN age INT;") error_rules = [f.rule for f in report.findings if f.level == Severity.ERROR] assert "ALTER_TABLE_PRODUCTION" not in error_rules # --- JOIN without ON --- def test_join_no_on_warning(self): report = self.engine.audit("SELECT * FROM users JOIN orders;") rules = [f.rule for f in report.findings] assert "JOIN_NO_ON" in rules def test_join_with_on_ok(self): report = self.engine.audit("SELECT * FROM users JOIN orders ON users.id = orders.user_id;") rules = [f.rule for f in report.findings] assert "JOIN_NO_ON" not in rules # --- Score --- def test_clean_sql_score_100(self): report = self.engine.audit("SELECT id, name FROM users WHERE id = 1 LIMIT 10;") assert report.score == 100 assert report.passed def test_error_reduces_score(self): report = self.engine.audit("UPDATE users SET name = 'test';") assert report.score < 100 assert not report.passed def test_report_to_dict(self): report = self.engine.audit("SELECT * FROM users;") d = report.to_dict() assert "passed" in d assert "score" in d assert "findings" in d assert isinstance(d["findings"], list) class TestAuditEngineIntegration: """Test audit engine integration with ticket creation via API.""" @pytest.mark.asyncio async def test_ticket_creation_returns_audit_report(self, client, db, admin_token, test_instance): from tests.conftest import auth_header resp = await client.post( "/api/v1/tickets", json={ "title": "SQL审核测试工单", "db_type": "mysql", "instance_id": test_instance.id, "target_database": "test_db", "sql_content": "SELECT * FROM users WHERE id = 1;", "reason": "测试审核引擎", "priority": "medium", }, headers=auth_header(admin_token), ) assert resp.status_code == 201 data = resp.json() assert "audit_report" in data assert "score" in data["audit_report"] assert "findings" in data["audit_report"] @pytest.mark.asyncio async def test_ticket_with_dangerous_sql_has_low_score(self, client, db, admin_token, test_instance): from tests.conftest import auth_header resp = await client.post( "/api/v1/tickets", json={ "title": "危险SQL工单", "db_type": "mysql", "instance_id": test_instance.id, "target_database": "test_db", "sql_content": "UPDATE users SET status = 1;", "reason": "全表更新测试", "priority": "high", }, headers=auth_header(admin_token), ) assert resp.status_code == 201 data = resp.json() report = data["audit_report"] assert report["passed"] is False assert report["score"] < 100