# #region Test.DatasourceMetadataService [C:3] [TYPE Module] [SEMANTICS test,datasource,metadata] # @BRIEF Tests for service_datasource.py — datasource metadata fetching. # @RELATION BINDS_TO -> [DatasourceMetadataService] # @TEST_EDGE: empty_backend -> ValueError # @TEST_EDGE: unsupported_dialect -> ValueError # @TEST_EDGE: no_env_config -> ValueError import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src")) import pytest from unittest.mock import AsyncMock, MagicMock, patch from src.plugins.translate.service_datasource import ( get_dialect_from_database, detect_virtual_columns, get_datasource_columns, fetch_datasource_metadata, SUPPORTED_DIALECTS, ) class TestGetDialectFromDatabase: """get_dialect_from_database — extract dialect from Superset DB record.""" def test_backend_key(self): """Uses 'backend' key.""" result = get_dialect_from_database({"backend": "postgresql"}) assert result == "postgresql" def test_engine_fallback(self): """Falls back to 'engine' when 'backend' missing.""" result = get_dialect_from_database({"engine": "mysql"}) assert result == "mysql" def test_empty_raises(self): """Empty backend/engine raises ValueError.""" with pytest.raises(ValueError, match="Could not determine"): get_dialect_from_database({}) def test_dialect_mapping(self): """Dialect map normalizes known backends.""" assert get_dialect_from_database({"backend": "greenplum"}) == "postgresql" assert get_dialect_from_database({"backend": "clickhousedb"}) == "clickhouse" def test_unsupported_dialect_raises(self): """Unsupported dialect raises ValueError.""" with pytest.raises(ValueError, match="Unsupported database dialect"): get_dialect_from_database({"backend": "mongodb"}) def test_case_insensitive(self): """Backend is case-insensitive.""" result = get_dialect_from_database({"backend": "PostgreSQL"}) assert result == "postgresql" class TestDetectVirtualColumns: """detect_virtual_columns — identify computed columns.""" def test_detects_virtual(self): """Returns names of non-physical columns.""" columns = [ {"name": "id", "is_physical": True}, {"name": "full_name", "is_physical": False}, {"name": "email", "is_physical": True}, ] virtual = detect_virtual_columns(columns) assert virtual == ["full_name"] def test_all_physical(self): """All physical returns empty list.""" columns = [ {"name": "id", "is_physical": True}, {"name": "name", "is_physical": True}, ] assert detect_virtual_columns(columns) == [] def test_all_virtual(self): """All virtual returns all names.""" columns = [ {"name": "calc1", "is_physical": False}, {"name": "calc2", "is_physical": False}, ] virtual = detect_virtual_columns(columns) assert virtual == ["calc1", "calc2"] def test_default_is_physical(self): """Default is_physical is True when missing.""" columns = [{"name": "id"}] assert detect_virtual_columns(columns) == [] class TestFetchDatasourceMetadata: """fetch_datasource_metadata — fetch columns + dialect.""" @pytest.mark.asyncio async def test_success_with_database_dict(self): """Success when database is a dict.""" config_manager = MagicMock() env_config = MagicMock() env_config.id = "env-1" config_manager.get_environments.return_value = [env_config] with patch("src.plugins.translate.service_datasource.get_superset_client") as mock_get: mock_client = AsyncMock() mock_get.return_value = mock_client mock_client.get_dataset_detail.return_value = { "columns": [ {"name": "id", "type": "INTEGER", "is_physical": True}, {"name": "name", "type": "VARCHAR", "is_physical": True}, ], "database": {"backend": "postgresql"}, } columns, dialect = await fetch_datasource_metadata(1, "env-1", config_manager) assert len(columns) == 2 assert dialect == "postgresql" assert columns[0]["name"] == "id" @pytest.mark.asyncio async def test_no_env_config(self): """Non-existent env raises ValueError.""" config_manager = MagicMock() config_manager.get_environments.return_value = [] with pytest.raises(ValueError, match="not found"): await fetch_datasource_metadata(1, "env-1", config_manager) @pytest.mark.asyncio async def test_columns_with_name_fallback(self): """Uses 'column_name' when 'name' is missing.""" config_manager = MagicMock() env_config = MagicMock() env_config.id = "env-1" config_manager.get_environments.return_value = [env_config] with patch("src.plugins.translate.service_datasource.get_superset_client") as mock_get: mock_client = AsyncMock() mock_get.return_value = mock_client mock_client.get_dataset_detail.return_value = { "columns": [ {"column_name": "my_col", "type": "VARCHAR"}, ], "database": {"backend": "postgresql"}, } columns, dialect = await fetch_datasource_metadata(1, "env-1", config_manager) assert len(columns) == 1 assert columns[0]["name"] == "my_col" @pytest.mark.asyncio async def test_skips_columns_without_name(self): """Skips columns without name or column_name.""" config_manager = MagicMock() env_config = MagicMock() env_config.id = "env-1" config_manager.get_environments.return_value = [env_config] with patch("src.plugins.translate.service_datasource.get_superset_client") as mock_get: mock_client = AsyncMock() mock_get.return_value = mock_client mock_client.get_dataset_detail.return_value = { "columns": [ {"name": "valid_col"}, {"extra": "no_name"}, {}, ], "database": {"backend": "postgresql"}, } columns, dialect = await fetch_datasource_metadata(1, "env-1", config_manager) assert len(columns) == 1 @pytest.mark.asyncio async def test_database_info_as_non_dict(self): """When database_info is not a dict (e.g., str), fetches from database_id.""" config_manager = MagicMock() env_config = MagicMock() env_config.id = "env-1" config_manager.get_environments.return_value = [env_config] with patch("src.plugins.translate.service_datasource.get_superset_client") as mock_get: mock_client = AsyncMock() mock_get.return_value = mock_client mock_client.get_dataset_detail.return_value = { "columns": [], "database": "not a dict", "database_id": 123, } mock_client.get_database.return_value = {"result": {"backend": "mysql"}} columns, dialect = await fetch_datasource_metadata(1, "env-1", config_manager) assert dialect == "mysql" @pytest.mark.asyncio async def test_database_info_not_dict_no_db_id(self): """When database_info not a dict and no database_id, raises.""" config_manager = MagicMock() env_config = MagicMock() env_config.id = "env-1" config_manager.get_environments.return_value = [env_config] with patch("src.plugins.translate.service_datasource.get_superset_client") as mock_get: mock_client = AsyncMock() mock_get.return_value = mock_client mock_client.get_dataset_detail.return_value = { "columns": [], "database": "not a dict", } with pytest.raises(ValueError, match="No database information"): await fetch_datasource_metadata(1, "env-1", config_manager) @pytest.mark.asyncio async def test_database_info_not_dict_db_error(self): """Exception when fetching database info raises.""" config_manager = MagicMock() env_config = MagicMock() env_config.id = "env-1" config_manager.get_environments.return_value = [env_config] with patch("src.plugins.translate.service_datasource.get_superset_client") as mock_get: mock_client = AsyncMock() mock_get.return_value = mock_client mock_client.get_dataset_detail.return_value = { "columns": [], "database": "not a dict", "database_id": 123, } mock_client.get_database.side_effect = Exception("API error") with pytest.raises(ValueError, match="Could not determine"): await fetch_datasource_metadata(1, "env-1", config_manager) class TestGetDatasourceColumns: """get_datasource_columns — structured column metadata response.""" @pytest.mark.asyncio async def test_success(self): """Returns structured response with columns.""" config_manager = MagicMock() env_config = MagicMock() env_config.id = "env-1" config_manager.get_environments.return_value = [env_config] with patch("src.plugins.translate.service_datasource.get_superset_client") as mock_get: mock_client = AsyncMock() mock_get.return_value = mock_client mock_client.get_dataset_detail.return_value = { "columns": [ {"name": "id", "type": "INTEGER"}, ], "database": {"backend": "postgresql"}, "table_name": "my_table", "schema": "public", } response = await get_datasource_columns(1, "env-1", config_manager) assert response.datasource_id == 1 assert response.database_dialect == "postgresql" assert response.datasource_name == "my_table" assert len(response.columns) == 1 assert response.columns[0].name == "id" @pytest.mark.asyncio async def test_no_env_config(self): """Non-existent env raises ValueError.""" config_manager = MagicMock() config_manager.get_environments.return_value = [] with pytest.raises(ValueError, match="not found"): await get_datasource_columns(1, "env-1", config_manager) @pytest.mark.asyncio async def test_dialect_from_database_fallback(self): """When dialect from database dict fails, fetches from database_id.""" config_manager = MagicMock() env_config = MagicMock() env_config.id = "env-1" config_manager.get_environments.return_value = [env_config] with patch("src.plugins.translate.service_datasource.get_superset_client") as mock_get: mock_client = AsyncMock() mock_get.return_value = mock_client # get_dialect_from_database will fail on empty backend mock_client.get_dataset_detail.return_value = { "columns": [], "database": {"not_backend": "mysql"}, "database_id": 456, } mock_client.get_database.return_value = {"result": {"backend": "mysql"}} response = await get_datasource_columns(1, "env-1", config_manager) assert response.database_dialect == "mysql" @pytest.mark.asyncio async def test_no_database_id_raises(self): """When dialect fails and no database_id, raises.""" config_manager = MagicMock() env_config = MagicMock() env_config.id = "env-1" config_manager.get_environments.return_value = [env_config] with patch("src.plugins.translate.service_datasource.get_superset_client") as mock_get: mock_client = AsyncMock() mock_get.return_value = mock_client mock_client.get_dataset_detail.return_value = { "columns": [], "database": {"not_backend": "xxx"}, } with pytest.raises(ValueError, match="Could not determine"): await get_datasource_columns(1, "env-1", config_manager) @pytest.mark.asyncio async def test_skips_column_without_name(self): """Columns without name or column_name are skipped (line 159).""" config_manager = MagicMock() env_config = MagicMock() env_config.id = "env-1" config_manager.get_environments.return_value = [env_config] with patch("src.plugins.translate.service_datasource.get_superset_client") as mock_get: mock_client = AsyncMock() mock_get.return_value = mock_client mock_client.get_dataset_detail.return_value = { "columns": [ {"name": "valid_col", "type": "INTEGER"}, {"not_name": "should_skip", "type": "VARCHAR"}, {}, {"column_name": "also_valid", "type": "TEXT"}, ], "database": {"backend": "postgresql"}, "table_name": "t1", "schema": "public", } response = await get_datasource_columns(1, "env-1", config_manager) assert len(response.columns) == 2 assert response.columns[0].name == "valid_col" assert response.columns[1].name == "also_valid" # #endregion Test.DatasourceMetadataService