v4: 7643 tests passing, 83% raw / 96.9% real coverage (excl __tests__).
12 known failures — all from Agent 3 new unverified tests (mock setup issues): - 3 dataset_review_routes_extended (DTO field mismatches) - 1 settings_consolidated (dict key access) - 1 llm_analysis_service_coverage (rate_limit mock) - 1 migration_plugin (SessionLocal side_effect exhaustion) - 1 preview (DB query vs dict key) - 5 scheduler (datetime timezone + async mock mismatches) NEW TEST FILES THIS SESSION: - test_batch_insert_coverage.py — 3 tests - test_storage_plugin.py — +3 tests - test_search.py — +2 tests - test_mapper.py — already 100% - test_llm_analysis_migration_v1_to_v2.py — 14 tests - test_llm_async_http.py — +1 test - test_prompt_builder.py — +1 test - test_service_datasource.py — +1 test - test_lang_detect.py — +1 test - test_scheduler.py — +6 tests - test_llm_analysis_service_coverage.py — +15 tests - test_dataset_review_routes_extended.py — +14 tests - test_settings_consolidated.py — +13 tests Modules pushed to 100%: _batch_insert, dictionary_entries, service_datasource, _llm_async_http, prompt_builder, dictionary_crud, _batch_sizer, storage/plugin, mapper.py Session: 7194→7643 tests (+449), 80%→83% raw (+3pp), 93.4%→96.9% real (+3.5pp). Remaining: 12 failures to fix + ~300 statements to reach 98% real coverage.
This commit is contained in:
346
backend/tests/plugins/test_mapper.py
Normal file
346
backend/tests/plugins/test_mapper.py
Normal file
@@ -0,0 +1,346 @@
|
||||
# #region Test.MapperPlugin [C:3] [TYPE Module] [SEMANTICS test, mapper, plugin, dataset, superset]
|
||||
# @BRIEF Unit tests for MapperPlugin — properties, schema, execute with sqllab/excel sources, error branches.
|
||||
# @RELATION BINDS_TO -> [MapperPlugin]
|
||||
# @TEST_EDGE: missing_params -> raises ValueError
|
||||
# @TEST_EDGE: env_not_found -> raises ValueError
|
||||
# @TEST_EDGE: sqllab_missing_database_id -> raises ValueError
|
||||
# @TEST_EDGE: upstream_exception -> re-raises
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from src.plugins.mapper import MapperPlugin
|
||||
|
||||
|
||||
# ── Helpers ──
|
||||
|
||||
|
||||
def _mock_env_config(name: str = "dev"):
|
||||
env = MagicMock()
|
||||
env.id = f"env-{name}"
|
||||
env.name = name
|
||||
return env
|
||||
|
||||
|
||||
def _mock_config_manager(env=None):
|
||||
cm = MagicMock()
|
||||
if env:
|
||||
cm.get_environment.return_value = env
|
||||
else:
|
||||
cm.get_environment.return_value = None
|
||||
cm.get_config.return_value = MagicMock()
|
||||
return cm
|
||||
|
||||
|
||||
def _mock_task_context():
|
||||
ctx = MagicMock()
|
||||
ctx.logger = MagicMock()
|
||||
ctx.logger.with_source.return_value = MagicMock()
|
||||
return ctx
|
||||
|
||||
|
||||
class TestMapperPluginProperties:
|
||||
"""Verify static property values."""
|
||||
|
||||
def test_id(self):
|
||||
plugin = MapperPlugin()
|
||||
assert plugin.id == "dataset-mapper"
|
||||
|
||||
def test_name(self):
|
||||
plugin = MapperPlugin()
|
||||
assert plugin.name == "Dataset Mapper"
|
||||
|
||||
def test_description(self):
|
||||
plugin = MapperPlugin()
|
||||
assert plugin.description == "Map dataset column verbose names using Superset SQL Lab or Excel files."
|
||||
|
||||
def test_version(self):
|
||||
plugin = MapperPlugin()
|
||||
assert plugin.version == "1.0.0"
|
||||
|
||||
def test_ui_route(self):
|
||||
plugin = MapperPlugin()
|
||||
assert plugin.ui_route == "/tools/mapper"
|
||||
|
||||
|
||||
class TestMapperPluginGetSchema:
|
||||
"""Verify get_schema output."""
|
||||
|
||||
def test_get_schema(self):
|
||||
plugin = MapperPlugin()
|
||||
schema = plugin.get_schema()
|
||||
|
||||
assert schema["type"] == "object"
|
||||
props = schema["properties"]
|
||||
assert "env" in props
|
||||
assert "dataset_id" in props
|
||||
assert "source" in props
|
||||
assert props["source"]["enum"] == ["sqllab", "excel"]
|
||||
assert props["source"]["default"] == "sqllab"
|
||||
assert schema["required"] == ["env", "dataset_id", "source"]
|
||||
|
||||
|
||||
class TestMapperPluginExecute:
|
||||
"""Verify MapperPlugin.execute with various scenarios."""
|
||||
|
||||
# ── Missing params ──
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_missing_all_params(self):
|
||||
"""No params at all raises ValueError."""
|
||||
plugin = MapperPlugin()
|
||||
with pytest.raises(ValueError, match="Missing required parameters"):
|
||||
await plugin.execute({})
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_missing_env(self):
|
||||
"""Missing env param raises ValueError."""
|
||||
plugin = MapperPlugin()
|
||||
with pytest.raises(ValueError, match="Missing required parameters"):
|
||||
await plugin.execute({"dataset_id": 1, "source": "sqllab"})
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_missing_dataset_id(self):
|
||||
"""None dataset_id raises ValueError."""
|
||||
plugin = MapperPlugin()
|
||||
with pytest.raises(ValueError, match="Missing required parameters"):
|
||||
await plugin.execute({"env": "dev", "source": "sqllab"})
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_missing_source(self):
|
||||
"""Missing source raises ValueError."""
|
||||
plugin = MapperPlugin()
|
||||
with pytest.raises(ValueError, match="Missing required parameters"):
|
||||
await plugin.execute({"env": "dev", "dataset_id": 1})
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_missing_params_with_context(self):
|
||||
"""Missing params also fails when context is provided."""
|
||||
plugin = MapperPlugin()
|
||||
ctx = _mock_task_context()
|
||||
with pytest.raises(ValueError, match="Missing required parameters"):
|
||||
await plugin.execute({"env": "dev"}, context=ctx)
|
||||
|
||||
# ── Environment not found ──
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_env_not_found(self):
|
||||
"""Environment not in config raises ValueError."""
|
||||
plugin = MapperPlugin()
|
||||
mock_cm = _mock_config_manager(env=None) # get_environment returns None
|
||||
|
||||
with patch("src.dependencies.get_config_manager", return_value=mock_cm):
|
||||
with pytest.raises(ValueError, match="not found"):
|
||||
await plugin.execute({"env": "nonexistent", "dataset_id": 1, "source": "sqllab"})
|
||||
|
||||
# ── SQLLab source — missing database_id ──
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_sqllab_missing_database_id(self):
|
||||
"""SQLLab source without database_id raises ValueError."""
|
||||
plugin = MapperPlugin()
|
||||
env = _mock_env_config("dev")
|
||||
mock_cm = _mock_config_manager(env=env)
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_client.authenticate = MagicMock()
|
||||
|
||||
with (
|
||||
patch("src.dependencies.get_config_manager", return_value=mock_cm),
|
||||
patch("src.plugins.mapper.SupersetClient", return_value=mock_client),
|
||||
):
|
||||
with pytest.raises(ValueError, match="database_id is required"):
|
||||
await plugin.execute({"env": "dev", "dataset_id": 1, "source": "sqllab"})
|
||||
|
||||
# ── SQLLab source — success ──
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_sqllab_success(self):
|
||||
"""SQLLab source completes successfully."""
|
||||
plugin = MapperPlugin()
|
||||
env = _mock_env_config("dev")
|
||||
mock_cm = _mock_config_manager(env=env)
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_client.authenticate = MagicMock()
|
||||
|
||||
mock_executor = AsyncMock()
|
||||
mock_executor.resolve_database_id = AsyncMock()
|
||||
|
||||
mock_mapper = MagicMock()
|
||||
mock_mapper.run_mapping = AsyncMock()
|
||||
|
||||
with (
|
||||
patch("src.dependencies.get_config_manager", return_value=mock_cm),
|
||||
patch("src.plugins.mapper.SupersetClient", return_value=mock_client),
|
||||
patch("src.plugins.translate.superset_executor.SupersetSqlLabExecutor", return_value=mock_executor),
|
||||
patch("src.plugins.mapper.DatasetMapper", return_value=mock_mapper),
|
||||
):
|
||||
result = await plugin.execute({
|
||||
"env": "dev",
|
||||
"dataset_id": 42,
|
||||
"source": "sqllab",
|
||||
"database_id": 10,
|
||||
"sql_query": "SELECT * FROM cols",
|
||||
})
|
||||
|
||||
assert result["status"] == "success"
|
||||
assert result["dataset_id"] == 42
|
||||
mock_executor.resolve_database_id.assert_called_once_with(target_database_id="10")
|
||||
mock_mapper.run_mapping.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_sqllab_success_with_context(self):
|
||||
"""SQLLab source with TaskContext."""
|
||||
plugin = MapperPlugin()
|
||||
env = _mock_env_config("dev")
|
||||
mock_cm = _mock_config_manager(env=env)
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_client.authenticate = MagicMock()
|
||||
|
||||
mock_executor = AsyncMock()
|
||||
mock_executor.resolve_database_id = AsyncMock()
|
||||
|
||||
mock_mapper = MagicMock()
|
||||
mock_mapper.run_mapping = AsyncMock()
|
||||
|
||||
ctx = _mock_task_context()
|
||||
|
||||
with (
|
||||
patch("src.dependencies.get_config_manager", return_value=mock_cm),
|
||||
patch("src.plugins.mapper.SupersetClient", return_value=mock_client),
|
||||
patch("src.plugins.translate.superset_executor.SupersetSqlLabExecutor", return_value=mock_executor),
|
||||
patch("src.plugins.mapper.DatasetMapper", return_value=mock_mapper),
|
||||
):
|
||||
result = await plugin.execute({
|
||||
"env": "dev",
|
||||
"dataset_id": 7,
|
||||
"source": "sqllab",
|
||||
"database_id": 3,
|
||||
}, context=ctx)
|
||||
|
||||
assert result["status"] == "success"
|
||||
assert result["dataset_id"] == 7
|
||||
ctx.logger.with_source.assert_called()
|
||||
|
||||
# ── Excel source — success ──
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_excel_success(self):
|
||||
"""Excel source completes successfully."""
|
||||
plugin = MapperPlugin()
|
||||
env = _mock_env_config("dev")
|
||||
mock_cm = _mock_config_manager(env=env)
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_client.authenticate = MagicMock()
|
||||
|
||||
mock_mapper = MagicMock()
|
||||
mock_mapper.run_mapping = AsyncMock()
|
||||
|
||||
with (
|
||||
patch("src.dependencies.get_config_manager", return_value=mock_cm),
|
||||
patch("src.plugins.mapper.SupersetClient", return_value=mock_client),
|
||||
patch("src.plugins.mapper.DatasetMapper", return_value=mock_mapper),
|
||||
):
|
||||
result = await plugin.execute({
|
||||
"env": "dev",
|
||||
"dataset_id": 99,
|
||||
"source": "excel",
|
||||
"excel_path": "/data/mapping.xlsx",
|
||||
})
|
||||
|
||||
assert result["status"] == "success"
|
||||
assert result["dataset_id"] == 99
|
||||
mock_mapper.run_mapping.assert_called_once_with(
|
||||
superset_client=mock_client,
|
||||
dataset_id=99,
|
||||
source="excel",
|
||||
excel_path="/data/mapping.xlsx",
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_excel_with_file_data(self):
|
||||
"""Excel source uses file_data fallback when excel_path is missing."""
|
||||
plugin = MapperPlugin()
|
||||
env = _mock_env_config("dev")
|
||||
mock_cm = _mock_config_manager(env=env)
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_client.authenticate = MagicMock()
|
||||
|
||||
mock_mapper = MagicMock()
|
||||
mock_mapper.run_mapping = AsyncMock()
|
||||
|
||||
with (
|
||||
patch("src.dependencies.get_config_manager", return_value=mock_cm),
|
||||
patch("src.plugins.mapper.SupersetClient", return_value=mock_client),
|
||||
patch("src.plugins.mapper.DatasetMapper", return_value=mock_mapper),
|
||||
):
|
||||
result = await plugin.execute({
|
||||
"env": "dev",
|
||||
"dataset_id": 100,
|
||||
"source": "excel",
|
||||
"file_data": b"raw_excel_data",
|
||||
})
|
||||
|
||||
assert result["status"] == "success"
|
||||
mock_mapper.run_mapping.assert_called_once_with(
|
||||
superset_client=mock_client,
|
||||
dataset_id=100,
|
||||
source="excel",
|
||||
excel_path=b"raw_excel_data",
|
||||
)
|
||||
|
||||
# ── Upstream exception propagation ──
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_upstream_exception_propagates(self):
|
||||
"""Exception from SupersetClient.authenticate re-raises."""
|
||||
plugin = MapperPlugin()
|
||||
env = _mock_env_config("dev")
|
||||
mock_cm = _mock_config_manager(env=env)
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_client.authenticate = MagicMock(side_effect=ConnectionError("API unreachable"))
|
||||
|
||||
with (
|
||||
patch("src.dependencies.get_config_manager", return_value=mock_cm),
|
||||
patch("src.plugins.mapper.SupersetClient", return_value=mock_client),
|
||||
):
|
||||
with pytest.raises(ConnectionError, match="API unreachable"):
|
||||
await plugin.execute({"env": "dev", "dataset_id": 1, "source": "sqllab"})
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_mapping_failure_propagates(self):
|
||||
"""Exception from run_mapping re-raises."""
|
||||
plugin = MapperPlugin()
|
||||
env = _mock_env_config("dev")
|
||||
mock_cm = _mock_config_manager(env=env)
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_client.authenticate = MagicMock()
|
||||
|
||||
mock_executor = AsyncMock()
|
||||
mock_executor.resolve_database_id = AsyncMock()
|
||||
|
||||
mock_mapper = MagicMock()
|
||||
mock_mapper.run_mapping = AsyncMock(side_effect=RuntimeError("Mapping crashed"))
|
||||
|
||||
with (
|
||||
patch("src.dependencies.get_config_manager", return_value=mock_cm),
|
||||
patch("src.plugins.mapper.SupersetClient", return_value=mock_client),
|
||||
patch("src.plugins.translate.superset_executor.SupersetSqlLabExecutor", return_value=mock_executor),
|
||||
patch("src.plugins.mapper.DatasetMapper", return_value=mock_mapper),
|
||||
):
|
||||
with pytest.raises(RuntimeError, match="Mapping crashed"):
|
||||
await plugin.execute({
|
||||
"env": "dev", "dataset_id": 1,
|
||||
"source": "sqllab", "database_id": 5,
|
||||
})
|
||||
# #endregion Test.MapperPlugin
|
||||
Reference in New Issue
Block a user