# #region Test.RunSourceFetcher [C:3] [TYPE Module] [SEMANTICS test, translate, source, fetch] # @BRIEF Tests for _run_source.py — fetch_source_rows, _extract_chart_data_rows. # @RELATION BINDS_TO -> [_run_source] # @TEST_EDGE: superset_datasource_fetch -> fetch_source_rows returns rows from Superset API # @TEST_EDGE: empty_datasource -> fallback to preview session # @TEST_EDGE: no_session -> returns empty list import pytest from unittest.mock import AsyncMock, MagicMock, patch from datetime import UTC, datetime from sqlalchemy import create_engine, event from sqlalchemy.orm import Session, sessionmaker from src.models.translate import ( Base, TranslationJob, TranslationPreviewSession, TranslationPreviewRecord, ) from src.plugins.translate._run_source import _extract_chart_data_rows def make_session(): engine = create_engine("sqlite:///:memory:") @event.listens_for(engine, "connect") def _set_sqlite_pragma(dbapi_connection, connection_record): cursor = dbapi_connection.cursor() cursor.execute("PRAGMA foreign_keys=ON") cursor.close() Base.metadata.create_all(bind=engine) session = sessionmaker(bind=engine)() return session, engine # #region TestExtractChartDataRows [C:1] [TYPE Class] class TestExtractChartDataRows: """_extract_chart_data_rows — extract data rows from Superset chart data API response.""" def test_result_list_with_data(self): """result is a list with item containing data key.""" response = {"result": [{"data": [{"x": 1}, {"x": 2}]}]} assert _extract_chart_data_rows(response) == [{"x": 1}, {"x": 2}] def test_result_dict_with_data(self): """result is a dict with data key.""" response = {"result": {"data": [{"x": 1}]}} assert _extract_chart_data_rows(response) == [{"x": 1}] def test_fallback_to_response_data(self): """Fallback to response.data.""" response = {"data": [{"x": 1}]} assert _extract_chart_data_rows(response) == [{"x": 1}] def test_result_list_no_data(self): """When result is a list with no data, return it.""" response = {"result": [{"x": 1}]} assert _extract_chart_data_rows(response) == [{"x": 1}] def test_empty_result_list(self): """Empty list returns empty list.""" assert _extract_chart_data_rows({"result": []}) == [] def test_empty_response(self): """No recognizable fields returns empty list.""" assert _extract_chart_data_rows({"foo": "bar"}) == [] # #endregion TestExtractChartDataRows # #region TestFetchSourceRows [C:3] [TYPE Class] class TestFetchSourceRows: """fetch_source_rows — fetch source rows from Superset datasource or preview.""" @pytest.mark.asyncio async def test_no_job_returns_empty(self): """No matching job -> empty list.""" session, engine = make_session() try: config_manager = MagicMock() from src.plugins.translate._run_source import fetch_source_rows result = await fetch_source_rows(session, config_manager, "nonexistent-job", "run-1") assert result == [] finally: session.close() Base.metadata.drop_all(bind=engine) @pytest.mark.asyncio async def test_job_without_datasource_preview_session(self): """Job with no datasource, but has preview session.""" session, engine = make_session() try: job = TranslationJob(id="job-1", name="Test", status="ACTIVE", source_dialect="en", target_dialect="fr", source_datasource_id=None) session.add(job) sess = TranslationPreviewSession(id="ps-1", job_id="job-1", status="APPLIED", created_at=datetime.now(UTC)) session.add(sess) session.flush() # Ensure TranslationPreviewSession is persisted before TranslationPreviewRecord rec = TranslationPreviewRecord(id="rec-1", session_id="ps-1", status="APPROVED", source_object_id="row1", source_sql="hello", source_object_name="Row 1") session.add(rec) session.commit() config_manager = MagicMock() from src.plugins.translate._run_source import fetch_source_rows result = await fetch_source_rows(session, config_manager, "job-1", "run-1") assert len(result) == 1 assert result[0]["source_text"] == "hello" # approved_translation should be the target_sql (None here since status=APPROVED but target_sql not set) assert result[0]["approved_translation"] is None finally: session.close() Base.metadata.drop_all(bind=engine) @pytest.mark.asyncio async def test_no_preview_session_returns_empty(self): """Job without datasource, no preview session -> empty list.""" session, engine = make_session() try: job = TranslationJob(id="job-2", name="Test", status="ACTIVE", source_dialect="en", target_dialect="fr", source_datasource_id=None) session.add(job) session.commit() config_manager = MagicMock() from src.plugins.translate._run_source import fetch_source_rows result = await fetch_source_rows(session, config_manager, "job-2", "run-2") assert result == [] finally: session.close() Base.metadata.drop_all(bind=engine) @pytest.mark.asyncio async def test_superset_datasource_success(self): """Job with datasource, successful fetch from Superset.""" session, engine = make_session() try: job = TranslationJob(id="job-3", name="Test", status="ACTIVE", source_dialect="en", target_dialect="fr", source_datasource_id=42, environment_id="env-1", translation_column="text") session.add(job) session.commit() config_manager = MagicMock() config_manager.get_environments.return_value = [ MagicMock(id="env-1") ] mock_client = MagicMock() mock_client.get_dataset_detail = AsyncMock(return_value={"id": 42}) mock_client.build_dataset_preview_query_context = MagicMock(return_value={ "queries": [{}], "form_data": {} }) mock_client.network.request = AsyncMock(return_value={ "result": [{"data": [{"text": "hello"}, {"text": "world"}]}] }) from src.plugins.translate._run_source import fetch_source_rows async def mock_get_superset_client(*args, **kwargs): return mock_client with patch('src.core.utils.client_registry.get_superset_client', new=mock_get_superset_client): result = await fetch_source_rows(session, config_manager, "job-3", "run-3") assert len(result) == 2 assert result[0]["source_text"] == "hello" assert result[1]["source_text"] == "world" assert result[0]["source_data"] == {"text": "hello"} finally: session.close() Base.metadata.drop_all(bind=engine) @pytest.mark.asyncio async def test_superset_fetch_falls_back_to_preview(self): """Superset fetch fails, falls back to preview session.""" session, engine = make_session() try: job = TranslationJob(id="job-4", name="Test", status="ACTIVE", source_dialect="en", target_dialect="fr", source_datasource_id=42, environment_id="env-1", translation_column="text") session.add(job) sess = TranslationPreviewSession(id="ps-4", job_id="job-4", status="APPLIED", created_at=datetime.now(UTC)) session.add(sess) session.flush() # Ensure TranslationPreviewSession is persisted before TranslationPreviewRecord rec = TranslationPreviewRecord(id="rec-4", session_id="ps-4", status="PENDING", source_object_id="row1", source_sql="fallback text", source_object_name="Row 1") session.add(rec) session.commit() config_manager = MagicMock() config_manager.get_environments.return_value = [ MagicMock(id="env-1") ] mock_client = AsyncMock() mock_client.get_dataset_detail.side_effect = Exception("API error") from src.plugins.translate._run_source import fetch_source_rows async def mock_get_superset_client(*args, **kwargs): return mock_client with patch('src.core.utils.client_registry.get_superset_client', new=mock_get_superset_client): result = await fetch_source_rows(session, config_manager, "job-4", "run-4") assert len(result) == 1 assert result[0]["source_text"] == "fallback text" finally: session.close() Base.metadata.drop_all(bind=engine) @pytest.mark.asyncio async def test_superset_empty_rows_falls_back(self): """Superset returns empty rows, falls back to preview.""" session, engine = make_session() try: job = TranslationJob(id="job-5", name="Test", status="ACTIVE", source_dialect="en", target_dialect="fr", source_datasource_id=42, environment_id="env-1", translation_column="text") session.add(job) sess = TranslationPreviewSession(id="ps-5", job_id="job-5", status="APPLIED", created_at=datetime.now(UTC)) session.add(sess) session.flush() # Ensure TranslationPreviewSession is persisted before TranslationPreviewRecord rec = TranslationPreviewRecord(id="rec-5", session_id="ps-5", status="APPROVED", source_object_id="row1", source_sql="fb text", source_object_name="Row 1") session.add(rec) session.commit() config_manager = MagicMock() config_manager.get_environments.return_value = [ MagicMock(id="env-1") ] mock_client = MagicMock() mock_client.get_dataset_detail = AsyncMock(return_value={"id": 42}) mock_client.build_dataset_preview_query_context = MagicMock(return_value={ "queries": [{}], "form_data": {} }) mock_client.network.request = AsyncMock(return_value={"result": []}) from src.plugins.translate._run_source import fetch_source_rows async def mock_get_superset_client(*args, **kwargs): return mock_client with patch('src.core.utils.client_registry.get_superset_client', new=mock_get_superset_client): result = await fetch_source_rows(session, config_manager, "job-5", "run-5") assert len(result) == 1 assert result[0]["source_text"] == "fb text" finally: session.close() Base.metadata.drop_all(bind=engine) @pytest.mark.asyncio async def test_superset_no_env_config_falls_back(self): """No matching environment config -> falls back to preview.""" session, engine = make_session() try: job = TranslationJob(id="job-6", name="Test", status="ACTIVE", source_dialect="en", target_dialect="fr", source_datasource_id=42, environment_id="nonexistent", translation_column="text") session.add(job) session.commit() config_manager = MagicMock() config_manager.get_environments.return_value = [ MagicMock(id="other-env") ] from src.plugins.translate._run_source import fetch_source_rows result = await fetch_source_rows(session, config_manager, "job-6", "run-6") # No preview session, falls back to empty assert result == [] finally: session.close() Base.metadata.drop_all(bind=engine) # #endregion TestFetchSourceRows # #endregion Test.RunSourceFetcher