| """Тесты на DbConnector и SqlExecutor. |
| |
| Покрывают чтение схемы SQLite-баз, генерацию DDL и проверку того, что |
| SQLite-подключение действительно открывается в режиме read-only — |
| модифицирующие операции должны падать с sqlite3.OperationalError. |
| """ |
|
|
| import sqlite3 |
| from pathlib import Path |
|
|
| import pytest |
|
|
| from src.db.connector import DbConnector, TableInfo |
| from src.db.executor import QueryResult, SqlExecutor |
|
|
|
|
| @pytest.fixture |
| def tiny_sqlite(tmp_path: Path) -> Path: |
| db = tmp_path / "tiny.sqlite" |
| conn = sqlite3.connect(db) |
| conn.execute( |
| "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, city TEXT)" |
| ) |
| conn.executemany( |
| "INSERT INTO users (id, name, city) VALUES (?, ?, ?)", |
| [(1, "Иван", "Казань"), (2, "Анна", "Москва"), (3, "Олег", "Казань")], |
| ) |
| conn.commit() |
| conn.close() |
| return db |
|
|
|
|
| |
| |
| |
|
|
| def test_connector_lists_tables(tiny_sqlite: Path): |
| c = DbConnector(str(tiny_sqlite)) |
| assert c.list_tables() == ["users"] |
|
|
|
|
| def test_connector_reads_columns(tiny_sqlite: Path): |
| c = DbConnector(str(tiny_sqlite)) |
| tables = c.get_schema(include_samples=False) |
| assert len(tables) == 1 |
| table = tables[0] |
| assert isinstance(table, TableInfo) |
| names = [col.name for col in table.columns] |
| assert names == ["id", "name", "city"] |
| |
| pk = next(col for col in table.columns if col.name == "id") |
| assert pk.primary_key is True |
| nn = next(col for col in table.columns if col.name == "name") |
| assert nn.nullable is False |
|
|
|
|
| def test_connector_renders_ddl(tiny_sqlite: Path): |
| c = DbConnector(str(tiny_sqlite)) |
| schema_text = c.render_schema(include_samples=True) |
| assert "CREATE TABLE users" in schema_text |
| assert "PRIMARY KEY" in schema_text |
| |
| assert "Иван" in schema_text or "Олег" in schema_text |
|
|
|
|
| def test_connector_accepts_sqlite_uri(tiny_sqlite: Path): |
| c = DbConnector(f"sqlite:///{tiny_sqlite}") |
| assert c.list_tables() == ["users"] |
|
|
|
|
| |
| |
| |
|
|
| def test_executor_runs_select(tiny_sqlite: Path): |
| ex = SqlExecutor(str(tiny_sqlite)) |
| res = ex.run("SELECT id, name FROM users ORDER BY id") |
| assert isinstance(res, QueryResult) |
| assert res.success |
| assert res.columns == ["id", "name"] |
| assert res.row_count == 3 |
| assert res.rows[0] == [1, "Иван"] |
|
|
|
|
| def test_executor_aggregation(tiny_sqlite: Path): |
| ex = SqlExecutor(str(tiny_sqlite)) |
| res = ex.run("SELECT city, COUNT(*) AS cnt FROM users GROUP BY city ORDER BY cnt DESC") |
| assert res.success |
| assert res.rows[0] == ["Казань", 2] |
|
|
|
|
| def test_executor_returns_error_on_bad_sql(tiny_sqlite: Path): |
| ex = SqlExecutor(str(tiny_sqlite)) |
| res = ex.run("SELEC nonsense FROM users") |
| assert not res.success |
| assert res.error is not None |
|
|
|
|
| def test_executor_blocks_modifications(tiny_sqlite: Path): |
| """Ключевая проверка: SQLite-соединение открывается в read-only |
| режиме (URI mode=ro&immutable=1), модифицирующие операции должны |
| падать ошибкой, а не выполняться втихую.""" |
| ex = SqlExecutor(str(tiny_sqlite)) |
|
|
| res = ex.run("DELETE FROM users WHERE id = 1") |
| assert not res.success |
| assert res.error is not None |
| assert "read" in res.error.lower() or "readonly" in res.error.lower() \ |
| or "только для чтения" in res.error.lower() |
|
|
| |
| check = ex.run("SELECT COUNT(*) FROM users") |
| assert check.success |
| assert check.rows == [[3]] |
|
|
|
|
| def test_executor_blocks_drop_table(tiny_sqlite: Path): |
| ex = SqlExecutor(str(tiny_sqlite)) |
| res = ex.run("DROP TABLE users") |
| assert not res.success |
|
|
| |
| check = ex.run("SELECT COUNT(*) FROM users") |
| assert check.success |
|
|
|
|
| def test_queryresult_to_markdown(tiny_sqlite: Path): |
| ex = SqlExecutor(str(tiny_sqlite)) |
| res = ex.run("SELECT id, name FROM users WHERE id = 1") |
| md = res.to_markdown_table() |
| assert "id" in md and "name" in md |
| assert "Иван" in md |
|
|