test(backend): add 55+ test files to push coverage to 98%

Subagents delivered tests across all uncovered backend modules:

Schemas (100%): agent, auth, health, profile, settings, validation
Services (98-100%): auth, profile, health, llm, mapping, resource,
  security, git, superset_lookup, sql_table_extractor, rbac
API routes (new): auth, admin, health, environments, plugins,
  dashboards (helpers, projection, actions, listing),
  git (config, deps, env, helpers)
Clean Release (100%): DTO, facade, policy_engine, stages,
  repos, preparation, source_isolation, compliance
Git services: base, remote_providers
Agent module: app, run, middleware, langgraph_setup
Core: trace, cleanup, ws_log_handler, timezone, auth (config/oauth/security), matching
Reports: normalizer, report_service, type_profiles
Notifications: service, providers

Also:
- .gitignore: add .coverage, *.cover, coverage-* dirs
- src/schemas/auth.py: fix AD group DN regex (comma in CN=...)
- Remove co-located src/services/__tests__/ (caused pytest module collision)
This commit is contained in:
2026-06-15 13:55:57 +03:00
parent 6989bb0bc9
commit ce0369ae5c
67 changed files with 12187 additions and 1993 deletions

View File

@@ -367,4 +367,285 @@ class TestRunBlocking:
await run_blocking("file", sync_fail)
# ── 9. asyncio.run() in running event loop regression ───────────────────
# Regression: _test_postgresql(), _execute_pg(), _fetch_pg() used asyncio.run()
# which raises RuntimeError when called from within a running event loop
# (e.g., FastAPI async def endpoint). Fixed: all methods converted to
# async def; asyncio.run() removed.
# Files: src/core/connection_service.py, src/core/db_executor.py
class TestAsyncioRunInRunningLoop:
"""Verify that migrated async methods work when called from a running event loop
(the scenario that previously crashed with 'asyncio.run() cannot be called from
a running event loop')."""
# ── 9a. Static structure: all migrated methods must be async def ──────────
def test_connection_service_test_connection_is_async(self):
"""test_connection must be a coroutine function (regression: was sync)."""
from src.core.connection_service import ConnectionService
assert asyncio.iscoroutinefunction(
ConnectionService.test_connection
), "test_connection must be async def — regression guard against asyncio.run()"
def test_connection_service_test_postgresql_is_async(self):
"""_test_postgresql must be a coroutine function (regression: used asyncio.run())."""
from src.core.connection_service import ConnectionService
assert asyncio.iscoroutinefunction(
ConnectionService._test_postgresql
), "_test_postgresql must be async def — regression guard against asyncio.run()"
def test_db_executor_execute_sql_is_async(self):
"""execute_sql must be a coroutine function (regression: was sync)."""
from src.core.db_executor import DbExecutor
assert asyncio.iscoroutinefunction(
DbExecutor.execute_sql
), "execute_sql must be async def — regression guard against asyncio.run()"
def test_db_executor_fetch_schema_is_async(self):
"""fetch_schema must be a coroutine function (regression: was sync)."""
from src.core.db_executor import DbExecutor
assert asyncio.iscoroutinefunction(
DbExecutor.fetch_schema
), "fetch_schema must be async def — regression guard against asyncio.run()"
def test_db_executor_execute_pg_is_async(self):
"""_execute_pg must be a coroutine function (regression: used asyncio.run())."""
from src.core.db_executor import DbExecutor
assert asyncio.iscoroutinefunction(
DbExecutor._execute_pg
), "_execute_pg must be async def — regression guard against asyncio.run()"
def test_db_executor_fetch_pg_is_async(self):
"""_fetch_pg must be a coroutine function (regression: used asyncio.run())."""
from src.core.db_executor import DbExecutor
assert asyncio.iscoroutinefunction(
DbExecutor._fetch_pg
), "_fetch_pg must be async def — regression guard against asyncio.run()"
# ── 9b. Functional: calling from running event loop doesn't crash ────────
@pytest.mark.asyncio
async def test_test_postgresql_works_in_running_loop(self):
"""_test_postgresql must not crash with 'asyncio.run() cannot be called'
when awaited from a running event loop. Tests the full asyncpg path."""
import sys
from src.core.config_models import DatabaseConnection
from src.core.connection_service import ConnectionService
cm = MagicMock()
cm.config.settings.connections = []
service = ConnectionService(cm)
conn = DatabaseConnection(
id="reg-asyncpg-1", name="Regression PG", host="localhost",
port=5432, database="test", username="u", password="p",
dialect="postgresql", pool_size=5,
)
# Mock asyncpg driver at EXT boundary
mock_pg_conn = AsyncMock()
mock_pg_conn.fetchrow = AsyncMock(return_value=["PostgreSQL 16.3"])
mock_asyncpg = MagicMock()
mock_asyncpg.connect = AsyncMock(return_value=mock_pg_conn)
with patch.dict(sys.modules, {"asyncpg": mock_asyncpg}):
# Before fix: RuntimeError: asyncio.run() cannot be called from a running event loop
result = await service._test_postgresql(conn)
assert result == "PostgreSQL 16.3"
mock_asyncpg.connect.assert_awaited_once_with(
host="localhost", port=5432, user="u", password="p",
database="test", timeout=10,
)
mock_pg_conn.fetchrow.assert_awaited_once_with("SELECT version()")
mock_pg_conn.close.assert_awaited_once()
@pytest.mark.asyncio
async def test_execute_pg_works_in_running_loop(self):
"""_execute_pg must not crash with 'asyncio.run() cannot be called'
when awaited from a running event loop. Tests the full asyncpg pool path."""
import sys
from src.core.db_executor import DbExecutor
conn = MagicMock()
conn.id = "reg-execpg-1"
conn.dialect = "postgresql"
conn.host = "localhost"
conn.port = 5432
conn.database = "test"
conn.username = "u"
conn.password = "p"
conn.pool_size = 5
conn.updated_at = "2025-01-01"
svc = MagicMock()
svc.get_connection.return_value = conn
executor = DbExecutor(svc)
# Mock asyncpg pool at EXT boundary
mock_pg_conn = AsyncMock()
mock_pg_conn.execute = AsyncMock(return_value="INSERT 0 5")
mock_pool_ctx = AsyncMock()
mock_pool_ctx.__aenter__ = AsyncMock(return_value=mock_pg_conn)
mock_pool_ctx.__aexit__ = AsyncMock(return_value=False)
mock_pool = MagicMock()
mock_pool.acquire.return_value = mock_pool_ctx
mock_asyncpg = MagicMock()
mock_asyncpg.create_pool = AsyncMock(return_value=mock_pool)
with patch.dict(sys.modules, {"asyncpg": mock_asyncpg}):
# Before fix: RuntimeError: asyncio.run() cannot be called from a running event loop
result = await executor._execute_pg(conn, "INSERT INTO t VALUES (1)", 0.0)
assert result.success is True
assert result.rows_affected == 5
mock_asyncpg.create_pool.assert_awaited_once()
mock_pg_conn.execute.assert_awaited_once_with("INSERT INTO t VALUES (1)")
@pytest.mark.asyncio
async def test_fetch_pg_works_in_running_loop(self):
"""_fetch_pg must not crash with 'asyncio.run() cannot be called'
when awaited from a running event loop. Tests the full asyncpg pool path."""
import sys
from src.core.db_executor import DbExecutor, DbSchemaColumn
conn = MagicMock()
conn.id = "reg-fetchpg-1"
conn.dialect = "postgresql"
conn.host = "localhost"
conn.port = 5432
conn.database = "test"
conn.username = "u"
conn.password = "p"
conn.pool_size = 5
conn.updated_at = "2025-01-01"
svc = MagicMock()
svc.get_connection.return_value = conn
executor = DbExecutor(svc)
# Mock asyncpg pool at EXT boundary
fake_rows = [
{"name": "id", "type": "integer"},
{"name": "email", "type": "varchar"},
]
mock_pg_conn = AsyncMock()
mock_pg_conn.fetch = AsyncMock(return_value=fake_rows)
mock_pool_ctx = AsyncMock()
mock_pool_ctx.__aenter__ = AsyncMock(return_value=mock_pg_conn)
mock_pool_ctx.__aexit__ = AsyncMock(return_value=False)
mock_pool = MagicMock()
mock_pool.acquire.return_value = mock_pool_ctx
mock_asyncpg = MagicMock()
mock_asyncpg.create_pool = AsyncMock(return_value=mock_pool)
with patch.dict(sys.modules, {"asyncpg": mock_asyncpg}):
# Before fix: RuntimeError: asyncio.run() cannot be called from a running event loop
result = await executor._fetch_pg(
conn, "SELECT column_name, data_type FROM information_schema.columns"
)
assert result is not None
assert len(result) == 2
assert result[0].name == "id"
assert result[0].data_type == "integer"
assert result[1].name == "email"
assert result[1].data_type == "varchar"
mock_asyncpg.create_pool.assert_awaited_once()
mock_pg_conn.fetch.assert_awaited_once()
@pytest.mark.asyncio
async def test_connection_service_test_connection_routes_to_async_pg(self):
"""test_connection must route to async _test_postgresql when dialect=postgresql
and work from a running event loop."""
from src.core.config_models import DatabaseConnection
from src.core.connection_service import ConnectionService
cm = MagicMock()
cm.config.settings.connections = []
service = ConnectionService(cm)
conn = DatabaseConnection(
id="reg-route-1", name="PG Route", host="localhost",
port=5432, database="test", username="u", password="p",
dialect="postgresql", pool_size=5,
)
cm.config.settings.connections.append(conn)
# Mock decryption to pass through
with patch.object(service, '_decrypt_password', return_value="p"), \
patch.object(service, '_test_postgresql', return_value="PostgreSQL 17"):
result = await service.test_connection("reg-route-1")
assert result["success"] is True
assert result["db_version"] == "PostgreSQL 17"
assert "latency_ms" in result
@pytest.mark.asyncio
async def test_execute_sql_pg_routes_to_async_execute_pg(self):
"""execute_sql must route to async _execute_pg when dialect=postgresql
and work from a running event loop."""
from unittest.mock import AsyncMock
from src.core.db_executor import DbExecutor, DbExecutionResult
conn = MagicMock()
conn.id = "reg-route-pg-1"
conn.dialect = "postgresql"
conn.host = "localhost"
conn.port = 5432
conn.database = "test"
conn.username = "u"
conn.password = "p"
conn.pool_size = 5
conn.updated_at = "2025-01-01"
svc = MagicMock()
svc.get_connection.return_value = conn
executor = DbExecutor(svc)
expected = DbExecutionResult(success=True, rows_affected=10, execution_time_ms=50)
mock_pg = AsyncMock(return_value=expected)
with patch.object(executor, '_execute_pg', mock_pg):
result = await executor.execute_sql("reg-route-pg-1", "INSERT INTO t VALUES (1)")
assert result.success is True
assert result.rows_affected == 10
# ── 9c. Rejected path: asyncio.run() on coroutine raises TypeError ───────
# Ensures that if someone mistakenly calls execute_sql() without await,
# the error is immediate and informative.
@pytest.mark.asyncio
async def test_async_method_without_await_returns_coroutine(self):
"""Calling execute_sql() without await returns a coroutine, not a result.
This guards the contract: the method IS async and must be awaited."""
from src.core.db_executor import DbExecutor
conn = MagicMock()
conn.id = "reg-coro-1"
conn.dialect = "clickhouse"
conn.updated_at = "2025-01-01"
svc = MagicMock()
svc.get_connection.return_value = conn
executor = DbExecutor(svc)
# Call without await → should return coroutine, NOT DbExecutionResult
coro = executor.execute_sql("reg-coro-1", "SELECT 1")
assert asyncio.iscoroutine(coro), (
"execute_sql() called without await must return a coroutine, "
"not a result — guards the contract that it's async def"
)
# #endregion TestAsyncRegression

View File

@@ -60,38 +60,34 @@ class TestExecuteSqlRouting:
# #region test_connection_not_found [C:2] [TYPE Function]
# @BRIEF execute_sql returns failure when connection_id is unknown.
def test_connection_not_found(self):
async def test_connection_not_found(self):
svc = _make_service(conn=None)
executor = DbExecutor(svc)
result = executor.execute_sql("nonexistent", "SELECT 1")
result = await executor.execute_sql("nonexistent", "SELECT 1")
assert result.success is False
assert "not found" in result.error
# #endregion test_connection_not_found
# #region test_unsupported_dialect [C:2] [TYPE Function]
# @BRIEF execute_sql returns failure for an unsupported dialect.
def test_unsupported_dialect(self):
async def test_unsupported_dialect(self):
conn = _make_conn(dialect="oracle")
svc = _make_service(conn)
executor = DbExecutor(svc)
result = executor.execute_sql(conn.id, "SELECT 1")
result = await executor.execute_sql(conn.id, "SELECT 1")
assert result.success is False
assert "Unsupported dialect" in result.error
# #endregion test_unsupported_dialect
# #region test_exception_during_execution [C:2] [TYPE Function]
# @BRIEF execute_sql catches driver exceptions and returns error with timing.
def test_exception_during_execution(self):
async def test_exception_during_execution(self):
conn = _make_conn(dialect="clickhouse")
svc = _make_service(conn)
executor = DbExecutor(svc)
with patch("src.core.db_executor.clickhouse_connect", create=True) as mock_mod:
# Force an ImportError to be caught inside _execute_ch's try block
# by making clickhouse_connect.get_client raise
pass
# Simpler: patch the entire _execute_ch to raise
with patch.object(executor, "_execute_ch", side_effect=RuntimeError("boom")):
result = executor.execute_sql(conn.id, "INSERT INTO t VALUES (1)")
result = await executor.execute_sql(conn.id, "INSERT INTO t VALUES (1)")
assert result.success is False
assert "boom" in result.error
assert result.execution_time_ms >= 0
@@ -107,7 +103,7 @@ class TestExecutePg:
# #region test_pg_success_insert [C:2] [TYPE Function]
# @BRIEF asyncpg returns 'INSERT 0 5' → rows_affected=5, success=True.
def test_pg_success_insert(self):
async def test_pg_success_insert(self):
conn = _make_conn(dialect="postgresql")
svc = _make_service(conn)
executor = DbExecutor(svc)
@@ -126,7 +122,7 @@ class TestExecutePg:
mock_asyncpg.create_pool = AsyncMock(return_value=mock_pool)
with patch.dict(sys.modules, {"asyncpg": mock_asyncpg}):
result = executor._execute_pg(conn, "INSERT INTO t VALUES (1)", 0.0)
result = await executor._execute_pg(conn, "INSERT INTO t VALUES (1)", 0.0)
assert result.success is True
assert result.rows_affected == 5
@@ -135,7 +131,7 @@ class TestExecutePg:
# #region test_pg_pool_reuse [C:2] [TYPE Function]
# @BRIEF Second call with same config reuses cached pool (create_pool called once).
def test_pg_pool_reuse(self):
async def test_pg_pool_reuse(self):
conn = _make_conn(dialect="postgresql")
svc = _make_service(conn)
executor = DbExecutor(svc)
@@ -154,15 +150,15 @@ class TestExecutePg:
mock_asyncpg.create_pool = AsyncMock(return_value=mock_pool)
with patch.dict(sys.modules, {"asyncpg": mock_asyncpg}):
executor._execute_pg(conn, "INSERT 1", 0.0)
executor._execute_pg(conn, "INSERT 2", 0.0)
await executor._execute_pg(conn, "INSERT 1", 0.0)
await executor._execute_pg(conn, "INSERT 2", 0.0)
assert mock_asyncpg.create_pool.call_count == 1
# #endregion test_pg_pool_reuse
# #region test_pg_pool_invalidation [C:2] [TYPE Function]
# @BRIEF Config change (updated_at) triggers pool recreation.
def test_pg_pool_invalidation(self):
async def test_pg_pool_invalidation(self):
conn = _make_conn(dialect="postgresql", updated_at="v1")
svc = _make_service(conn)
executor = DbExecutor(svc)
@@ -179,17 +175,17 @@ class TestExecutePg:
mock_asyncpg.create_pool = AsyncMock(return_value=mock_pool)
with patch.dict(sys.modules, {"asyncpg": mock_asyncpg}):
executor._execute_pg(conn, "INSERT 1", 0.0)
await executor._execute_pg(conn, "INSERT 1", 0.0)
# Simulate config update
conn.updated_at = "v2"
executor._execute_pg(conn, "INSERT 2", 0.0)
await executor._execute_pg(conn, "INSERT 2", 0.0)
assert mock_asyncpg.create_pool.call_count == 2
# #endregion test_pg_pool_invalidation
# #region test_pg_status_empty [C:2] [TYPE Function]
# @BRIEF asyncpg returns empty status string → rows_affected=0.
def test_pg_status_empty(self):
async def test_pg_status_empty(self):
conn = _make_conn(dialect="postgresql")
svc = _make_service(conn)
executor = DbExecutor(svc)
@@ -207,7 +203,7 @@ class TestExecutePg:
mock_asyncpg.create_pool = AsyncMock(return_value=mock_pool)
with patch.dict(sys.modules, {"asyncpg": mock_asyncpg}):
result = executor._execute_pg(conn, "CREATE TABLE t()", 0.0)
result = await executor._execute_pg(conn, "CREATE TABLE t()", 0.0)
assert result.success is True
assert result.rows_affected == 0
@@ -215,7 +211,7 @@ class TestExecutePg:
# #region test_pg_status_non_numeric [C:2] [TYPE Function]
# @BRIEF asyncpg returns non-numeric status → rows_affected=0 (ValueError caught).
def test_pg_status_non_numeric(self):
async def test_pg_status_non_numeric(self):
conn = _make_conn(dialect="postgresql")
svc = _make_service(conn)
executor = DbExecutor(svc)
@@ -233,7 +229,7 @@ class TestExecutePg:
mock_asyncpg.create_pool = AsyncMock(return_value=mock_pool)
with patch.dict(sys.modules, {"asyncpg": mock_asyncpg}):
result = executor._execute_pg(conn, "BEGIN", 0.0)
result = await executor._execute_pg(conn, "BEGIN", 0.0)
assert result.success is True
assert result.rows_affected == 0
@@ -241,7 +237,7 @@ class TestExecutePg:
# #region test_pg_status_single_part [C:2] [TYPE Function]
# @BRIEF asyncpg returns single-word status → rows_affected=0 (len(parts) < 2).
def test_pg_status_single_part(self):
async def test_pg_status_single_part(self):
conn = _make_conn(dialect="postgresql")
svc = _make_service(conn)
executor = DbExecutor(svc)
@@ -259,7 +255,7 @@ class TestExecutePg:
mock_asyncpg.create_pool = AsyncMock(return_value=mock_pool)
with patch.dict(sys.modules, {"asyncpg": mock_asyncpg}):
result = executor._execute_pg(conn, "COMMIT", 0.0)
result = await executor._execute_pg(conn, "COMMIT", 0.0)
assert result.success is True
assert result.rows_affected == 0
@@ -267,7 +263,7 @@ class TestExecutePg:
# #region test_pg_status_non_numeric_last_part [C:2] [TYPE Function]
# @BRIEF asyncpg returns multi-part status with non-numeric tail → ValueError caught, rows=0.
def test_pg_status_non_numeric_last_part(self):
async def test_pg_status_non_numeric_last_part(self):
conn = _make_conn(dialect="postgresql")
svc = _make_service(conn)
executor = DbExecutor(svc)
@@ -285,7 +281,7 @@ class TestExecutePg:
mock_asyncpg.create_pool = AsyncMock(return_value=mock_pool)
with patch.dict(sys.modules, {"asyncpg": mock_asyncpg}):
result = executor._execute_pg(conn, "INSERT INTO t VALUES (1)", 0.0)
result = await executor._execute_pg(conn, "INSERT INTO t VALUES (1)", 0.0)
assert result.success is True
assert result.rows_affected == 0
@@ -293,7 +289,7 @@ class TestExecutePg:
# #region test_pg_import_error [C:2] [TYPE Function]
# @BRIEF Missing asyncpg package → returns error result.
def test_pg_import_error(self):
async def test_pg_import_error(self):
conn = _make_conn(dialect="postgresql")
svc = _make_service(conn)
executor = DbExecutor(svc)
@@ -308,7 +304,7 @@ class TestExecutePg:
return real_import(name, *args, **kwargs)
with patch.object(builtins, "__import__", side_effect=fake_import):
result = executor._execute_pg(conn, "SELECT 1", 0.0)
result = await executor._execute_pg(conn, "SELECT 1", 0.0)
assert result.success is False
assert "asyncpg" in result.error
@@ -504,20 +500,20 @@ class TestFetchSchema:
# #region test_fetch_schema_connection_not_found [C:2] [TYPE Function]
# @BRIEF fetch_schema returns None when connection_id is unknown.
def test_fetch_schema_connection_not_found(self):
async def test_fetch_schema_connection_not_found(self):
executor = DbExecutor(MagicMock())
mock_svc = MagicMock()
mock_svc.get_connection.return_value = None
with patch("src.core.connection_service.ConnectionService", return_value=mock_svc):
result = executor.fetch_schema("nonexistent", "public", "users", MagicMock())
result = await executor.fetch_schema("nonexistent", "public", "users", MagicMock())
assert result is None
# #endregion test_fetch_schema_connection_not_found
# #region test_fetch_schema_clickhouse [C:2] [TYPE Function]
# @BRIEF fetch_schema routes to _fetch_ch for clickhouse dialect.
def test_fetch_schema_clickhouse(self):
async def test_fetch_schema_clickhouse(self):
conn = _make_conn(dialect="clickhouse")
executor = DbExecutor(MagicMock())
mock_svc = MagicMock()
@@ -526,7 +522,7 @@ class TestFetchSchema:
expected = [DbSchemaColumn(name="id", data_type="UInt64")]
with patch("src.core.connection_service.ConnectionService", return_value=mock_svc), \
patch.object(executor, "_fetch_ch", return_value=expected) as mock_fetch:
result = executor.fetch_schema("ch-1", "default", "events", MagicMock())
result = await executor.fetch_schema("ch-1", "default", "events", MagicMock())
assert result == expected
mock_fetch.assert_called_once()
@@ -537,16 +533,17 @@ class TestFetchSchema:
# #region test_fetch_schema_postgresql [C:2] [TYPE Function]
# @BRIEF fetch_schema routes to _fetch_pg for postgresql dialect.
def test_fetch_schema_postgresql(self):
async def test_fetch_schema_postgresql(self):
conn = _make_conn(dialect="postgresql")
executor = DbExecutor(MagicMock())
mock_svc = MagicMock()
mock_svc.get_connection.return_value = conn
expected = [DbSchemaColumn(name="id", data_type="integer")]
mock_fetch = AsyncMock(return_value=expected)
with patch("src.core.connection_service.ConnectionService", return_value=mock_svc), \
patch.object(executor, "_fetch_pg", return_value=expected) as mock_fetch:
result = executor.fetch_schema("pg-1", "public", "users", MagicMock())
patch.object(executor, "_fetch_pg", mock_fetch):
result = await executor.fetch_schema("pg-1", "public", "users", MagicMock())
assert result == expected
mock_fetch.assert_called_once()
@@ -556,7 +553,7 @@ class TestFetchSchema:
# #region test_fetch_schema_mysql [C:2] [TYPE Function]
# @BRIEF fetch_schema routes to _fetch_mysql for mysql dialect.
def test_fetch_schema_mysql(self):
async def test_fetch_schema_mysql(self):
conn = _make_conn(dialect="mysql")
executor = DbExecutor(MagicMock())
mock_svc = MagicMock()
@@ -565,7 +562,7 @@ class TestFetchSchema:
expected = [DbSchemaColumn(name="id", data_type="int")]
with patch("src.core.connection_service.ConnectionService", return_value=mock_svc), \
patch.object(executor, "_fetch_mysql", return_value=expected) as mock_fetch:
result = executor.fetch_schema("my-1", "mydb", "users", MagicMock())
result = await executor.fetch_schema("my-1", "mydb", "users", MagicMock())
assert result == expected
mock_fetch.assert_called_once()
@@ -575,29 +572,30 @@ class TestFetchSchema:
# #region test_fetch_schema_unsupported_dialect [C:2] [TYPE Function]
# @BRIEF fetch_schema returns None for unsupported dialect.
def test_fetch_schema_unsupported_dialect(self):
async def test_fetch_schema_unsupported_dialect(self):
conn = _make_conn(dialect="oracle")
executor = DbExecutor(MagicMock())
mock_svc = MagicMock()
mock_svc.get_connection.return_value = conn
with patch("src.core.connection_service.ConnectionService", return_value=mock_svc):
result = executor.fetch_schema("ora-1", "dbo", "users", MagicMock())
result = await executor.fetch_schema("ora-1", "dbo", "users", MagicMock())
assert result is None
# #endregion test_fetch_schema_unsupported_dialect
# #region test_fetch_schema_sql_injection_escaping [C:2] [TYPE Function]
# @BRIEF fetch_schema escapes single quotes in schema/table names.
def test_fetch_schema_sql_injection_escaping(self):
async def test_fetch_schema_sql_injection_escaping(self):
conn = _make_conn(dialect="postgresql")
executor = DbExecutor(MagicMock())
mock_svc = MagicMock()
mock_svc.get_connection.return_value = conn
mock_fetch = AsyncMock(return_value=[])
with patch("src.core.connection_service.ConnectionService", return_value=mock_svc), \
patch.object(executor, "_fetch_pg", return_value=[]) as mock_fetch:
executor.fetch_schema("pg-1", "pub'lic", "us'ers", MagicMock())
patch.object(executor, "_fetch_pg", mock_fetch):
await executor.fetch_schema("pg-1", "pub'lic", "us'ers", MagicMock())
call_sql = mock_fetch.call_args[0][1]
assert "pub''lic" in call_sql
@@ -614,7 +612,7 @@ class TestFetchPg:
# #region test_fetch_pg_success [C:2] [TYPE Function]
# @BRIEF asyncpg fetch returns rows → mapped to DbSchemaColumn list.
def test_fetch_pg_success(self):
async def test_fetch_pg_success(self):
conn = _make_conn(dialect="postgresql", conn_id="pg-f1")
svc = _make_service(conn)
executor = DbExecutor(svc)
@@ -638,7 +636,7 @@ class TestFetchPg:
mock_asyncpg.create_pool = AsyncMock(return_value=mock_pool)
with patch.dict(sys.modules, {"asyncpg": mock_asyncpg}):
result = executor._fetch_pg(conn, "SELECT column_name, data_type FROM information_schema.columns")
result = await executor._fetch_pg(conn, "SELECT column_name, data_type FROM information_schema.columns")
assert result is not None
assert len(result) == 2
@@ -650,7 +648,7 @@ class TestFetchPg:
# #region test_fetch_pg_import_error [C:2] [TYPE Function]
# @BRIEF Missing asyncpg → _fetch_pg returns None.
def test_fetch_pg_import_error(self):
async def test_fetch_pg_import_error(self):
conn = _make_conn(dialect="postgresql", conn_id="pg-f2")
svc = _make_service(conn)
executor = DbExecutor(svc)
@@ -664,7 +662,7 @@ class TestFetchPg:
return real_import(name, *args, **kwargs)
with patch.object(builtins, "__import__", side_effect=fake_import):
result = executor._fetch_pg(conn, "SELECT 1")
result = await executor._fetch_pg(conn, "SELECT 1")
assert result is None
# #endregion test_fetch_pg_import_error
@@ -853,14 +851,15 @@ class TestExecuteSqlDialects:
# #region test_execute_sql_postgresql [C:2] [TYPE Function]
# @BRIEF execute_sql routes postgresql to _execute_pg and returns its result.
def test_execute_sql_postgresql(self):
async def test_execute_sql_postgresql(self):
conn = _make_conn(dialect="postgresql")
svc = _make_service(conn)
executor = DbExecutor(svc)
expected = DbExecutionResult(success=True, rows_affected=10, execution_time_ms=50.0)
with patch.object(executor, "_execute_pg", return_value=expected) as mock_pg:
result = executor.execute_sql(conn.id, "INSERT INTO t VALUES (1)")
mock_pg = AsyncMock(return_value=expected)
with patch.object(executor, "_execute_pg", mock_pg):
result = await executor.execute_sql(conn.id, "INSERT INTO t VALUES (1)")
assert result.success is True
assert result.rows_affected == 10
@@ -869,14 +868,14 @@ class TestExecuteSqlDialects:
# #region test_execute_sql_clickhouse [C:2] [TYPE Function]
# @BRIEF execute_sql routes clickhouse to _execute_ch and returns its result.
def test_execute_sql_clickhouse(self):
async def test_execute_sql_clickhouse(self):
conn = _make_conn(dialect="clickhouse")
svc = _make_service(conn)
executor = DbExecutor(svc)
expected = DbExecutionResult(success=True, rows_affected=7, execution_time_ms=30.0)
with patch.object(executor, "_execute_ch", return_value=expected) as mock_ch:
result = executor.execute_sql(conn.id, "INSERT INTO t VALUES (1)")
result = await executor.execute_sql(conn.id, "INSERT INTO t VALUES (1)")
assert result.success is True
assert result.rows_affected == 7
@@ -885,14 +884,14 @@ class TestExecuteSqlDialects:
# #region test_execute_sql_mysql [C:2] [TYPE Function]
# @BRIEF execute_sql routes mysql to _execute_mysql and returns its result.
def test_execute_sql_mysql(self):
async def test_execute_sql_mysql(self):
conn = _make_conn(dialect="mysql")
svc = _make_service(conn)
executor = DbExecutor(svc)
expected = DbExecutionResult(success=True, rows_affected=3, execution_time_ms=20.0)
with patch.object(executor, "_execute_mysql", return_value=expected) as mock_my:
result = executor.execute_sql(conn.id, "INSERT INTO t VALUES (1)")
result = await executor.execute_sql(conn.id, "INSERT INTO t VALUES (1)")
assert result.success is True
assert result.rows_affected == 3