# #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