diff --git a/backend/src/plugins/translate/__tests__/test_dialect_detection_orthogonal.py b/backend/src/plugins/translate/__tests__/test_dialect_detection_orthogonal.py index 85297d20..2284394e 100644 --- a/backend/src/plugins/translate/__tests__/test_dialect_detection_orthogonal.py +++ b/backend/src/plugins/translate/__tests__/test_dialect_detection_orthogonal.py @@ -11,7 +11,7 @@ import pytest from typing import Any -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, patch from src.plugins.translate.service_utils import _extract_dialect from src.plugins.translate.service_datasource import ( @@ -648,7 +648,7 @@ class TestSchemaValidationDialectRouting: # region test_clickhouse_uses_system_columns [TYPE Function] # @PURPOSE: ClickHouse backend generates system.columns query. @patch("src.plugins.translate.service_target_schema.SupersetSqlLabExecutor") - def test_clickhouse_uses_system_columns(self, mock_executor) -> None: + async def test_clickhouse_uses_system_columns(self, mock_executor) -> None: from src.schemas.translate import TargetSchemaValidationRequest from src.plugins.translate.service_target_schema import validate_target_table_schema @@ -663,10 +663,10 @@ class TestSchemaValidationDialectRouting: config_manager = MagicMock() instance = mock_executor.return_value - instance.resolve_database_id.return_value = None + instance.resolve_database_id = AsyncMock(return_value=None) instance.get_database_backend.return_value = "clickhouse" instance.get_database_name.return_value = "test_ch" - instance.execute_and_poll.return_value = { + instance.execute_and_poll = AsyncMock(return_value={ "status": "success", "raw_response": { "data": [ @@ -676,9 +676,9 @@ class TestSchemaValidationDialectRouting: {"name": "is_original", "type": "UInt8"}, ], }, - } + }) - validate_target_table_schema(req, config_manager) + await validate_target_table_schema(req, config_manager) # Verify the SQL query used system.columns call_args = instance.execute_and_poll.call_args @@ -690,7 +690,7 @@ class TestSchemaValidationDialectRouting: # region test_postgresql_uses_information_schema [TYPE Function] # @PURPOSE: PostgreSQL backend generates information_schema.columns query. @patch("src.plugins.translate.service_target_schema.SupersetSqlLabExecutor") - def test_postgresql_uses_information_schema(self, mock_executor) -> None: + async def test_postgresql_uses_information_schema(self, mock_executor) -> None: from src.schemas.translate import TargetSchemaValidationRequest from src.plugins.translate.service_target_schema import validate_target_table_schema @@ -705,10 +705,10 @@ class TestSchemaValidationDialectRouting: config_manager = MagicMock() instance = mock_executor.return_value - instance.resolve_database_id.return_value = None + instance.resolve_database_id = AsyncMock(return_value=None) instance.get_database_backend.return_value = "postgresql" instance.get_database_name.return_value = "test_pg" - instance.execute_and_poll.return_value = { + instance.execute_and_poll = AsyncMock(return_value={ "status": "success", "raw_response": { "data": [ @@ -718,9 +718,9 @@ class TestSchemaValidationDialectRouting: {"column_name": "is_original", "data_type": "boolean"}, ], }, - } + }) - validate_target_table_schema(req, config_manager) + await validate_target_table_schema(req, config_manager) call_args = instance.execute_and_poll.call_args sql = call_args.kwargs.get("sql") or call_args.args[0] @@ -731,7 +731,7 @@ class TestSchemaValidationDialectRouting: # region test_clickhousedb_normalized_to_clickhouse [TYPE Function] # @PURPOSE: "clickhousedb" backend is normalized to clickhouse -> uses system.columns. @patch("src.plugins.translate.service_target_schema.SupersetSqlLabExecutor") - def test_clickhousedb_normalized_to_clickhouse(self, mock_executor) -> None: + async def test_clickhousedb_normalized_to_clickhouse(self, mock_executor) -> None: from src.schemas.translate import TargetSchemaValidationRequest from src.plugins.translate.service_target_schema import validate_target_table_schema @@ -746,10 +746,10 @@ class TestSchemaValidationDialectRouting: config_manager = MagicMock() instance = mock_executor.return_value - instance.resolve_database_id.return_value = None + instance.resolve_database_id = AsyncMock(return_value=None) instance.get_database_backend.return_value = "clickhousedb" instance.get_database_name.return_value = "test_ch" - instance.execute_and_poll.return_value = { + instance.execute_and_poll = AsyncMock(return_value={ "status": "success", "raw_response": { "data": [ @@ -759,9 +759,9 @@ class TestSchemaValidationDialectRouting: {"name": "is_original", "type": "UInt8"}, ], }, - } + }) - validate_target_table_schema(req, config_manager) + await validate_target_table_schema(req, config_manager) call_args = instance.execute_and_poll.call_args sql = call_args.kwargs.get("sql") or call_args.args[0] @@ -771,7 +771,7 @@ class TestSchemaValidationDialectRouting: # region test_greenplum_normalized_to_postgresql [TYPE Function] # @PURPOSE: "greenplum" backend is normalized to postgresql -> uses information_schema. @patch("src.plugins.translate.service_target_schema.SupersetSqlLabExecutor") - def test_greenplum_normalized_to_postgresql(self, mock_executor) -> None: + async def test_greenplum_normalized_to_postgresql(self, mock_executor) -> None: from src.schemas.translate import TargetSchemaValidationRequest from src.plugins.translate.service_target_schema import validate_target_table_schema @@ -786,10 +786,10 @@ class TestSchemaValidationDialectRouting: config_manager = MagicMock() instance = mock_executor.return_value - instance.resolve_database_id.return_value = None + instance.resolve_database_id = AsyncMock(return_value=None) instance.get_database_backend.return_value = "greenplum" instance.get_database_name.return_value = "test_gp" - instance.execute_and_poll.return_value = { + instance.execute_and_poll = AsyncMock(return_value={ "status": "success", "raw_response": { "data": [ @@ -799,9 +799,9 @@ class TestSchemaValidationDialectRouting: {"name": "is_original", "type": "boolean"}, ], }, - } + }) - validate_target_table_schema(req, config_manager) + await validate_target_table_schema(req, config_manager) call_args = instance.execute_and_poll.call_args sql = call_args.kwargs.get("sql") or call_args.args[0] @@ -811,7 +811,7 @@ class TestSchemaValidationDialectRouting: # region test_redshift_uses_information_schema [TYPE Function] # @PURPOSE: Redshift uses information_schema (it's PostgreSQL-derived). @patch("src.plugins.translate.service_target_schema.SupersetSqlLabExecutor") - def test_redshift_uses_information_schema(self, mock_executor) -> None: + async def test_redshift_uses_information_schema(self, mock_executor) -> None: from src.schemas.translate import TargetSchemaValidationRequest from src.plugins.translate.service_target_schema import validate_target_table_schema @@ -826,10 +826,10 @@ class TestSchemaValidationDialectRouting: config_manager = MagicMock() instance = mock_executor.return_value - instance.resolve_database_id.return_value = None + instance.resolve_database_id = AsyncMock(return_value=None) instance.get_database_backend.return_value = "redshift" instance.get_database_name.return_value = "test_rs" - instance.execute_and_poll.return_value = { + instance.execute_and_poll = AsyncMock(return_value={ "status": "success", "raw_response": { "data": [ @@ -839,9 +839,9 @@ class TestSchemaValidationDialectRouting: {"column_name": "is_original", "data_type": "boolean"}, ], }, - } + }) - validate_target_table_schema(req, config_manager) + await validate_target_table_schema(req, config_manager) call_args = instance.execute_and_poll.call_args sql = call_args.kwargs.get("sql") or call_args.args[0] @@ -851,7 +851,7 @@ class TestSchemaValidationDialectRouting: # region test_case_insensitive_backend_normalization [TYPE Function] # @PURPOSE: Backend normalization is case-insensitive. @patch("src.plugins.translate.service_target_schema.SupersetSqlLabExecutor") - def test_case_insensitive_backend_normalization(self, mock_executor) -> None: + async def test_case_insensitive_backend_normalization(self, mock_executor) -> None: from src.schemas.translate import TargetSchemaValidationRequest from src.plugins.translate.service_target_schema import validate_target_table_schema @@ -866,10 +866,10 @@ class TestSchemaValidationDialectRouting: config_manager = MagicMock() instance = mock_executor.return_value - instance.resolve_database_id.return_value = None + instance.resolve_database_id = AsyncMock(return_value=None) instance.get_database_backend.return_value = "ClickHouseDB" instance.get_database_name.return_value = "test_ch" - instance.execute_and_poll.return_value = { + instance.execute_and_poll = AsyncMock(return_value={ "status": "success", "raw_response": { "data": [ @@ -879,9 +879,9 @@ class TestSchemaValidationDialectRouting: {"name": "is_original", "type": "UInt8"}, ], }, - } + }) - validate_target_table_schema(req, config_manager) + await validate_target_table_schema(req, config_manager) call_args = instance.execute_and_poll.call_args sql = call_args.kwargs.get("sql") or call_args.args[0] diff --git a/backend/src/plugins/translate/__tests__/test_target_schema.py b/backend/src/plugins/translate/__tests__/test_target_schema.py index 0970bf1a..2a94bee5 100644 --- a/backend/src/plugins/translate/__tests__/test_target_schema.py +++ b/backend/src/plugins/translate/__tests__/test_target_schema.py @@ -9,7 +9,7 @@ # @TEST_CONTRACT validate_target_table_schema -> TargetSchemaValidationResponse import pytest -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, patch from src.plugins.translate.service_target_schema import ( _build_expected_columns, @@ -271,16 +271,16 @@ class TestParseSqlLabResult: class TestValidateTargetTableSchema: @patch("src.plugins.translate.service_target_schema.SupersetSqlLabExecutor") - def test_all_columns_present(self, mock_executor, build_request) -> None: + async def test_all_columns_present(self, mock_executor, build_request) -> None: """Все ожидаемые колонки присутствуют в таблице.""" req = build_request() config_manager = MagicMock() instance = mock_executor.return_value - instance.resolve_database_id.return_value = None + instance.resolve_database_id = AsyncMock(return_value=None) instance.get_database_backend.return_value = "postgresql" instance.get_database_name.return_value = "test_db" - instance.execute_and_poll.return_value = { + instance.execute_and_poll = AsyncMock(return_value={ "status": "success", "raw_response": { "columns": [{"name": "column_name"}, {"name": "data_type"}], @@ -291,9 +291,9 @@ class TestValidateTargetTableSchema: {"column_name": "is_original", "data_type": "boolean"}, ], }, - } + }) - response = validate_target_table_schema(req, config_manager) + response = await validate_target_table_schema(req, config_manager) assert response.table_exists is True assert response.error is None assert response.all_present is True @@ -304,16 +304,16 @@ class TestValidateTargetTableSchema: assert response.database_backend == "postgresql" @patch("src.plugins.translate.service_target_schema.SupersetSqlLabExecutor") - def test_missing_columns(self, mock_executor, build_request) -> None: + async def test_missing_columns(self, mock_executor, build_request) -> None: """В таблице не хватает некоторых ожидаемых колонок.""" req = build_request() config_manager = MagicMock() instance = mock_executor.return_value - instance.resolve_database_id.return_value = None + instance.resolve_database_id = AsyncMock(return_value=None) instance.get_database_backend.return_value = "postgresql" instance.get_database_name.return_value = "test_db" - instance.execute_and_poll.return_value = { + instance.execute_and_poll = AsyncMock(return_value={ "status": "success", "raw_response": { "columns": [{"name": "column_name"}], @@ -322,9 +322,9 @@ class TestValidateTargetTableSchema: {"column_name": "context"}, ], }, - } + }) - response = validate_target_table_schema(req, config_manager) + response = await validate_target_table_schema(req, config_manager) assert response.table_exists is True assert response.all_present is False missing_names = {c.name for c in response.missing_columns} @@ -333,7 +333,7 @@ class TestValidateTargetTableSchema: assert "id" not in missing_names @patch("src.plugins.translate.service_target_schema.SupersetSqlLabExecutor") - def test_clickhouse_describe_table(self, mock_executor, build_request) -> None: + async def test_clickhouse_describe_table(self, mock_executor, build_request) -> None: """ClickHouse использует DESCRIBE TABLE — другой формат ответа.""" req = build_request( target_key_cols=["report_date", "document_number"], @@ -345,10 +345,10 @@ class TestValidateTargetTableSchema: config_manager = MagicMock() instance = mock_executor.return_value - instance.resolve_database_id.return_value = None + instance.resolve_database_id = AsyncMock(return_value=None) instance.get_database_backend.return_value = "clickhouse" instance.get_database_name.return_value = "test_clickhouse" - instance.execute_and_poll.return_value = { + instance.execute_and_poll = AsyncMock(return_value={ "status": "success", "raw_response": { "columns": [{"name": "name"}, {"name": "type"}], @@ -363,9 +363,9 @@ class TestValidateTargetTableSchema: {"name": "is_original", "type": "UInt8"}, ], }, - } + }) - response = validate_target_table_schema(req, config_manager) + response = await validate_target_table_schema(req, config_manager) assert response.table_exists is True assert response.error is None assert response.all_present is True @@ -375,85 +375,85 @@ class TestValidateTargetTableSchema: assert response.database_backend == "clickhouse" @patch("src.plugins.translate.service_target_schema.SupersetSqlLabExecutor") - def test_superset_failed_status(self, mock_executor, build_request) -> None: + async def test_superset_failed_status(self, mock_executor, build_request) -> None: """Superset возвращает failed статус.""" req = build_request() config_manager = MagicMock() instance = mock_executor.return_value - instance.resolve_database_id.return_value = None + instance.resolve_database_id = AsyncMock(return_value=None) instance.get_database_backend.return_value = "postgresql" instance.get_database_name.return_value = "test_db" - instance.execute_and_poll.return_value = { + instance.execute_and_poll = AsyncMock(return_value={ "status": "failed", "error_message": "Query timeout", - } + }) - response = validate_target_table_schema(req, config_manager) + response = await validate_target_table_schema(req, config_manager) assert response.table_exists is False assert response.error is not None assert "Query timeout" in response.error assert response.all_present is False @patch("src.plugins.translate.service_target_schema.SupersetSqlLabExecutor") - def test_executor_exception(self, mock_executor, build_request) -> None: + async def test_executor_exception(self, mock_executor, build_request) -> None: """Executor бросает исключение.""" req = build_request() config_manager = MagicMock() instance = mock_executor.return_value - instance.resolve_database_id.return_value = None + instance.resolve_database_id = AsyncMock(return_value=None) instance.get_database_backend.return_value = "postgresql" instance.get_database_name.return_value = "test_db" - instance.execute_and_poll.side_effect = ConnectionError("Superset API unavailable") + instance.execute_and_poll = AsyncMock(side_effect=ConnectionError("Superset API unavailable")) - response = validate_target_table_schema(req, config_manager) + response = await validate_target_table_schema(req, config_manager) assert response.table_exists is False assert response.error is not None assert "Superset API unavailable" in response.error assert response.all_present is False - def test_sql_injection_table_name(self, build_request) -> None: + async def test_sql_injection_table_name(self, build_request) -> None: """Невалидное имя таблицы с пробелом — ошибка.""" req = build_request(target_table="my table; DROP TABLE students; --") config_manager = MagicMock() - response = validate_target_table_schema(req, config_manager) + response = await validate_target_table_schema(req, config_manager) assert response.table_exists is False assert response.error is not None assert "Invalid target table name" in response.error assert response.all_present is False - def test_sql_injection_schema_name(self, build_request) -> None: + async def test_sql_injection_schema_name(self, build_request) -> None: """Невалидное имя схемы с DROP — ошибка.""" req = build_request(target_schema="public; DROP TABLE students; --") config_manager = MagicMock() - response = validate_target_table_schema(req, config_manager) + response = await validate_target_table_schema(req, config_manager) assert response.table_exists is False assert response.error is not None assert "Invalid target schema name" in response.error assert response.all_present is False @patch("src.plugins.translate.service_target_schema.SupersetSqlLabExecutor") - def test_table_not_exists(self, mock_executor, build_request) -> None: + async def test_table_not_exists(self, mock_executor, build_request) -> None: """Таблица не существует — пустой data.""" req = build_request() config_manager = MagicMock() instance = mock_executor.return_value - instance.resolve_database_id.return_value = None + instance.resolve_database_id = AsyncMock(return_value=None) instance.get_database_backend.return_value = "postgresql" instance.get_database_name.return_value = "test_db" - instance.execute_and_poll.return_value = { + instance.execute_and_poll = AsyncMock(return_value={ "status": "success", "raw_response": { "columns": [{"name": "column_name"}], "data": [], }, - } + }) - response = validate_target_table_schema(req, config_manager) + response = await validate_target_table_schema(req, config_manager) assert response.table_exists is False assert response.error is None assert response.all_present is False