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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user