feat: durcit l'exécution SQL IA, borne les uploads et ajoute des tests
- sql_executor: remplace le filtre regex fragile par une autorisation SQLite (set_authorizer) en complément de mode=ro ; rejette les instructions multiples - uploads: lecture bornée des PDF (helper read_upload_limited, limite 20 Mo, HTTP 413) branchée sur /extract et /save-with-pdf - tests: suite pytest (54 tests) couvrant amounts, dates, storage, sql_executor, uploads et DatabaseService.save_document ; pytest ajouté en dépendance dev Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
94
tests/test_sql_executor.py
Normal file
94
tests/test_sql_executor.py
Normal file
@@ -0,0 +1,94 @@
|
||||
"""Tests de l'exécution SQL read-only de l'assistant IA."""
|
||||
|
||||
import sqlite3
|
||||
|
||||
import pytest
|
||||
|
||||
from plesna_gerance.services.sql_executor import (
|
||||
execute_readonly_sql,
|
||||
_validate_sql,
|
||||
_ensure_limit,
|
||||
_authorizer,
|
||||
_SQLITE_OK,
|
||||
_SQLITE_DENY,
|
||||
_SQLITE_SELECT,
|
||||
_SQLITE_READ,
|
||||
_SQLITE_PRAGMA,
|
||||
MAX_ROWS,
|
||||
)
|
||||
|
||||
|
||||
# --- Garde lexicale -------------------------------------------------------
|
||||
|
||||
|
||||
def test_validate_rejects_non_select():
|
||||
with pytest.raises(ValueError):
|
||||
_validate_sql("DELETE FROM tags")
|
||||
|
||||
|
||||
def test_validate_rejects_stacked_statements():
|
||||
with pytest.raises(ValueError):
|
||||
_validate_sql("SELECT 1; DROP TABLE tags")
|
||||
|
||||
|
||||
def test_validate_rejects_unknown_pragma():
|
||||
with pytest.raises(ValueError):
|
||||
_validate_sql("PRAGMA writable_schema = ON")
|
||||
|
||||
|
||||
def test_validate_allows_select_and_with():
|
||||
_validate_sql("SELECT * FROM tags")
|
||||
_validate_sql("WITH t AS (SELECT 1) SELECT * FROM t")
|
||||
_validate_sql("PRAGMA table_info(tags)")
|
||||
|
||||
|
||||
def test_ensure_limit_adds_limit():
|
||||
assert _ensure_limit("SELECT * FROM tags").endswith(f"LIMIT {MAX_ROWS}")
|
||||
|
||||
|
||||
def test_ensure_limit_preserves_existing():
|
||||
q = "SELECT * FROM tags LIMIT 5"
|
||||
assert _ensure_limit(q) == q
|
||||
|
||||
|
||||
# --- Autorisation SQLite (unitaire) ---------------------------------------
|
||||
|
||||
|
||||
def test_authorizer_allows_reads():
|
||||
assert _authorizer(_SQLITE_SELECT, None, None, None, None) == _SQLITE_OK
|
||||
assert _authorizer(_SQLITE_READ, "tags", "nom", "main", None) == _SQLITE_OK
|
||||
|
||||
|
||||
def test_authorizer_allows_whitelisted_pragma():
|
||||
assert _authorizer(_SQLITE_PRAGMA, "table_info", "tags", None, None) == _SQLITE_OK
|
||||
|
||||
|
||||
def test_authorizer_denies_unknown_pragma():
|
||||
assert _authorizer(_SQLITE_PRAGMA, "writable_schema", "ON", None, None) == _SQLITE_DENY
|
||||
|
||||
|
||||
def test_authorizer_denies_unknown_action():
|
||||
# 9 = SQLITE_DELETE, doit être refusé
|
||||
assert _authorizer(9, "tags", None, "main", None) == _SQLITE_DENY
|
||||
|
||||
|
||||
# --- Exécution réelle (nécessite une base) --------------------------------
|
||||
|
||||
|
||||
def test_execute_select_returns_rows(db_session):
|
||||
# init_db seed des tags prédéfinis
|
||||
result = execute_readonly_sql("SELECT nom FROM tags ORDER BY nom")
|
||||
assert "nom" in result["columns"]
|
||||
assert result["row_count"] >= 1
|
||||
|
||||
|
||||
def test_execute_write_blocked(db_session):
|
||||
# Bloqué par la garde lexicale (ValueError) ou, à défaut, par mode=ro /
|
||||
# l'autorisation au niveau SQLite (DatabaseError). Dans tous les cas : refusé.
|
||||
with pytest.raises((ValueError, sqlite3.DatabaseError)):
|
||||
execute_readonly_sql("DELETE FROM tags")
|
||||
|
||||
|
||||
def test_execute_attach_blocked(db_session):
|
||||
with pytest.raises((ValueError, sqlite3.DatabaseError)):
|
||||
execute_readonly_sql("ATTACH DATABASE 'x.db' AS x")
|
||||
Reference in New Issue
Block a user