PlainSQL / backend /tests /test_safety.py
LalitChaudhari3's picture
feat: synchronize text-to-sql-bot codebase with Hugging Face Space repository, including Docker build configurations
6086e71
Raw
History Blame Contribute Delete
4.45 kB
"""
Tests for the SQL Safety Validation Agent.
Verifies that dangerous queries are blocked and safe queries pass.
"""
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from app.agents.sql_validation import sql_validation_node, route_validation
class TestSQLValidation:
"""Test suite for SQL safety validation."""
def _validate(self, sql: str) -> dict:
"""Helper: run validation on a SQL string."""
state = {"generated_sql": sql, "retry_count": 0, "trace_id": "test"}
return sql_validation_node(state)
# ── Safe Queries (should PASS) ───────────────────────
def test_simple_select(self):
result = self._validate("SELECT * FROM employees LIMIT 10;")
assert result["is_valid"] is True
def test_select_with_where(self):
result = self._validate("SELECT name, salary FROM employees WHERE salary > 50000;")
assert result["is_valid"] is True
def test_select_with_join(self):
result = self._validate(
"SELECT e.name, d.name FROM employees e JOIN departments d ON e.department_id = d.id;"
)
assert result["is_valid"] is True
def test_select_with_aggregation(self):
result = self._validate(
"SELECT department_id, AVG(salary) as avg_sal FROM employees GROUP BY department_id;"
)
assert result["is_valid"] is True
def test_with_cte(self):
result = self._validate(
"WITH top_emp AS (SELECT * FROM employees ORDER BY salary DESC LIMIT 5) SELECT * FROM top_emp;"
)
assert result["is_valid"] is True
# ── Dangerous Queries (should BLOCK) ─────────────────
def test_block_drop_table(self):
result = self._validate("DROP TABLE employees;")
assert result["is_valid"] is False
assert any("DROP" in e.upper() for e in result["validation_errors"])
def test_block_delete(self):
result = self._validate("DELETE FROM employees WHERE id = 1;")
assert result["is_valid"] is False
def test_block_update(self):
result = self._validate("UPDATE employees SET salary = 0;")
assert result["is_valid"] is False
def test_block_insert(self):
result = self._validate("INSERT INTO employees (name) VALUES ('hacker');")
assert result["is_valid"] is False
def test_block_truncate(self):
result = self._validate("TRUNCATE TABLE employees;")
assert result["is_valid"] is False
def test_block_multi_statement_injection(self):
result = self._validate("SELECT * FROM employees; DROP TABLE employees;")
assert result["is_valid"] is False
def test_block_sleep_injection(self):
result = self._validate("SELECT SLEEP(10);")
assert result["is_valid"] is False
# ── LIMIT Injection ──────────────────────────────────
def test_limit_injection_when_missing(self):
result = self._validate("SELECT * FROM employees;")
assert result["is_valid"] is True
assert "LIMIT" in result["sanitized_sql"].upper()
def test_preserves_existing_limit(self):
result = self._validate("SELECT * FROM employees LIMIT 5;")
assert result["is_valid"] is True
assert "LIMIT 5" in result["sanitized_sql"]
# ── Route Validation ─────────────────────────────────
def test_route_valid(self):
state = {"is_valid": True, "retry_count": 0}
assert route_validation(state) == "valid"
def test_route_retry(self):
state = {"is_valid": False, "retry_count": 1}
assert route_validation(state) == "retry"
def test_route_blocked_after_max_retries(self):
state = {"is_valid": False, "retry_count": 3}
assert route_validation(state) == "blocked"
# ── Empty / Invalid Input ────────────────────────────
def test_empty_query(self):
result = self._validate("")
assert result["is_valid"] is False
def test_none_query(self):
state = {"generated_sql": None, "retry_count": 0, "trace_id": "test"}
result = sql_validation_node(state)
assert result["is_valid"] is False