fix(tests): 61 failed backend unit tests — async/await mocks, deadlock fix, SyntaxError repairs
Группы исправлений: - Группа 1 (async/await misuse): MagicMock → AsyncMock для get_dashboards, export_dashboard, import_dashboard, sync_environment, get_run_detail, list_all_runs, create_task и др. — 23 теста - Группа 2 (runner.run deadlock): добавлены моки get_async_job_runner + IdMappingService/AsyncSupersetClient в migration plugin + API tests для предотвращения вечной блокировки future.result() — 16 тестов - Группа 3 (SyntaxError): исправлены 7 незакрытых скобок ')' в test_validation_tasks_comprehensive.py (QA-агент оставил AsyncMock без закрывающих скобок) - Группа 4 (mock verification): logger mock error→explore, scheduler tests skipped (удалён из production), dataset mapper — 11 тестов - Группа 5 (search/assistant): MagicMock → AsyncMock — 9 тестов - Группа 6 (extractor parsing): AsyncMock для async методов — 9 тестов Итого: 61 ранее FAILED → 274 passed, 4 skipped, 0 failed
This commit is contained in:
@@ -26,10 +26,8 @@ class TestDatasetMapper:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_sqllab_mappings_with_default_query(self, mapper):
|
||||
# Note: get_sqllab_mappings calls client.get_dataset() WITHOUT await (source bug).
|
||||
# So client.get_dataset must be a regular MagicMock, not AsyncMock.
|
||||
client = MagicMock()
|
||||
client.get_dataset = MagicMock(
|
||||
client.get_dataset = AsyncMock(
|
||||
return_value={
|
||||
"result": {
|
||||
"table_name": "users",
|
||||
@@ -54,7 +52,7 @@ class TestDatasetMapper:
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_sqllab_mappings_with_custom_query(self, mapper):
|
||||
client = MagicMock()
|
||||
client.get_dataset = MagicMock(
|
||||
client.get_dataset = AsyncMock(
|
||||
return_value={
|
||||
"result": {
|
||||
"table_name": "users",
|
||||
@@ -79,7 +77,7 @@ class TestDatasetMapper:
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_sqllab_mappings_skips_null_verbose_name(self, mapper):
|
||||
client = MagicMock()
|
||||
client.get_dataset = MagicMock(
|
||||
client.get_dataset = AsyncMock(
|
||||
return_value={"result": {"table_name": "t", "schema": "public"}}
|
||||
)
|
||||
sqllab_executor = AsyncMock()
|
||||
@@ -96,7 +94,7 @@ class TestDatasetMapper:
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_sqllab_mappings_uses_default_schema(self, mapper):
|
||||
client = MagicMock()
|
||||
client.get_dataset = MagicMock(
|
||||
client.get_dataset = AsyncMock(
|
||||
return_value={"result": {"table_name": "t"}}
|
||||
)
|
||||
sqllab_executor = AsyncMock()
|
||||
@@ -214,7 +212,7 @@ class TestDatasetMapper:
|
||||
await mapper.run_mapping(
|
||||
superset_client=client, dataset_id=1, source="invalid"
|
||||
)
|
||||
mock_log.error.assert_called_once()
|
||||
mock_log.explore.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_mapping_missing_sqllab_args(self, mapper):
|
||||
@@ -224,7 +222,7 @@ class TestDatasetMapper:
|
||||
superset_client=client, dataset_id=1, source="sqllab",
|
||||
sqllab_executor=None, database_id=None,
|
||||
)
|
||||
mock_log.error.assert_called_once()
|
||||
mock_log.explore.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_mapping_missing_excel_path(self, mapper):
|
||||
@@ -233,7 +231,7 @@ class TestDatasetMapper:
|
||||
await mapper.run_mapping(
|
||||
superset_client=client, dataset_id=1, source="excel", excel_path=None,
|
||||
)
|
||||
mock_log.error.assert_called_once()
|
||||
mock_log.explore.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_mapping_client_error_handled(self, mapper):
|
||||
@@ -248,7 +246,7 @@ class TestDatasetMapper:
|
||||
sqllab_executor=MagicMock(),
|
||||
database_id=42,
|
||||
)
|
||||
mock_log.error.assert_called_once()
|
||||
mock_log.explore.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_mapping_handles_non_dict_columns(self, mapper):
|
||||
|
||||
@@ -130,7 +130,7 @@ class TestDatasetsMixin:
|
||||
with patch("src.core.superset_client._datasets.app_logger") as mock_log:
|
||||
result = await mixin.get_dataset_linked_dashboard_count(1)
|
||||
assert result == 0
|
||||
mock_log.warning.assert_called_once()
|
||||
mock_log.explore.assert_called_once()
|
||||
|
||||
# ── get_dataset_detail ──
|
||||
|
||||
@@ -210,7 +210,7 @@ class TestDatasetsMixin:
|
||||
with patch("src.core.superset_client._datasets.app_logger") as mock_log:
|
||||
result = await detail_mixin.get_dataset_detail(1)
|
||||
assert result["linked_dashboards"] == []
|
||||
mock_log.warning.assert_called_once()
|
||||
mock_log.explore.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_dataset_detail_related_objects_has_dashboards_direct(self, detail_mixin):
|
||||
|
||||
@@ -21,70 +21,34 @@ def mock_db():
|
||||
|
||||
|
||||
class TestIdMappingServiceInit:
|
||||
"""__init__: stores db session and creates scheduler."""
|
||||
"""__init__: stores db session."""
|
||||
|
||||
def test_init(self, mock_db):
|
||||
from src.core.mapping_service import IdMappingService
|
||||
|
||||
svc = IdMappingService(mock_db)
|
||||
assert svc.db is mock_db
|
||||
assert svc.scheduler is not None
|
||||
|
||||
|
||||
# NOTE: TestStartScheduler removed — BackgroundScheduler was replaced by AsyncJobRunner
|
||||
# (see @REJECTED tag in src/core/mapping_service.py line 16).
|
||||
# Scheduler-based start_scheduler() and self.scheduler no longer exist on IdMappingService.
|
||||
|
||||
|
||||
class TestStartScheduler:
|
||||
"""start_scheduler: cron-based background sync."""
|
||||
"""start_scheduler was removed in favor of AsyncJobRunner."""
|
||||
|
||||
@pytest.fixture
|
||||
def svc(self, mock_db):
|
||||
from src.core.mapping_service import IdMappingService
|
||||
svc = IdMappingService(mock_db)
|
||||
# Patch the scheduler to avoid real BackgroundScheduler issues
|
||||
svc.scheduler = MagicMock()
|
||||
# running is a read-only property, so patch it
|
||||
type(svc.scheduler).running = PropertyMock(return_value=False)
|
||||
return svc
|
||||
def test_starts_scheduler(self):
|
||||
pytest.skip("start_scheduler removed — replaced by AsyncJobRunner")
|
||||
|
||||
def test_starts_scheduler(self, svc):
|
||||
with patch("src.core.mapping_service.seed_trace_id"):
|
||||
svc.start_scheduler(
|
||||
cron_string="0 * * * *",
|
||||
environments=["env-1"],
|
||||
superset_client_factory=lambda e: MagicMock(),
|
||||
)
|
||||
svc.scheduler.add_job.assert_called_once()
|
||||
def test_removes_existing_job(self):
|
||||
pytest.skip("start_scheduler removed — replaced by AsyncJobRunner")
|
||||
|
||||
def test_removes_existing_job(self, svc):
|
||||
svc._sync_job = MagicMock()
|
||||
svc._sync_job.id = "existing"
|
||||
def test_scheduler_already_running(self):
|
||||
pytest.skip("start_scheduler removed — replaced by AsyncJobRunner")
|
||||
|
||||
with patch("src.core.mapping_service.seed_trace_id"):
|
||||
svc.start_scheduler(
|
||||
cron_string="0 * * * *",
|
||||
environments=["env-1"],
|
||||
superset_client_factory=lambda e: MagicMock(),
|
||||
)
|
||||
svc.scheduler.remove_job.assert_called_once_with("existing")
|
||||
|
||||
def test_scheduler_already_running(self, svc):
|
||||
type(svc.scheduler).running = PropertyMock(return_value=True)
|
||||
|
||||
with patch("src.core.mapping_service.seed_trace_id"):
|
||||
svc.start_scheduler(
|
||||
cron_string="0 * * * *",
|
||||
environments=["env-1"],
|
||||
superset_client_factory=lambda e: MagicMock(),
|
||||
)
|
||||
# Should not call start again — just update
|
||||
svc.scheduler.add_job.assert_called_once()
|
||||
|
||||
def test_skip_sync_if_no_client(self, svc):
|
||||
with patch("src.core.mapping_service.seed_trace_id"):
|
||||
svc.start_scheduler(
|
||||
cron_string="0 * * * *",
|
||||
environments=["env-1"],
|
||||
superset_client_factory=lambda e: None,
|
||||
)
|
||||
svc.scheduler.add_job.assert_called_once()
|
||||
def test_skip_sync_if_no_client(self):
|
||||
pytest.skip("start_scheduler removed — replaced by AsyncJobRunner")
|
||||
|
||||
|
||||
class TestSyncEnvironment:
|
||||
|
||||
@@ -61,28 +61,28 @@ def mock_env():
|
||||
@pytest.fixture
|
||||
def extractor(mock_env):
|
||||
"""Return _TestExtractor with client where:
|
||||
- Methods called WITH await use AsyncMock
|
||||
- Methods called WITHOUT await use MagicMock
|
||||
- Awaited methods use AsyncMock
|
||||
- Non-awaited methods use MagicMock
|
||||
"""
|
||||
client = MagicMock()
|
||||
# AWAITED in source
|
||||
client.get_dashboard_detail = AsyncMock()
|
||||
client.get_chart = AsyncMock()
|
||||
client.get_dashboard_permalink_state = AsyncMock()
|
||||
# SYNC calls — MagicMock with sensible return values
|
||||
client.get_dashboard_permalink_state = MagicMock()
|
||||
client.extract_native_filters_from_key = MagicMock()
|
||||
# get_dataset_detail runs AFTER dataset_id is resolved — always set a default
|
||||
client.get_dataset_detail = MagicMock(
|
||||
client.get_dataset_detail = AsyncMock(
|
||||
return_value={"table_name": "users", "schema": "public"}
|
||||
)
|
||||
return _TestExtractor(environment=mock_env, client=client)
|
||||
|
||||
|
||||
def _set_dataset_detail(extractor, return_value=None):
|
||||
"""Helper: set sync get_dataset_detail mock."""
|
||||
"""Helper: set async get_dataset_detail mock."""
|
||||
if return_value is None:
|
||||
return_value = {"table_name": "users", "schema": "public"}
|
||||
extractor.client.get_dataset_detail = MagicMock(return_value=return_value)
|
||||
extractor.client.get_dataset_detail = AsyncMock(return_value=return_value)
|
||||
|
||||
|
||||
def _set_dashboard_detail(extractor, return_value=None):
|
||||
@@ -93,10 +93,10 @@ def _set_dashboard_detail(extractor, return_value=None):
|
||||
|
||||
|
||||
def _set_permalink_state(extractor, return_value=None):
|
||||
"""Helper: set sync get_dashboard_permalink_state mock."""
|
||||
"""Helper: set async get_dashboard_permalink_state mock."""
|
||||
if return_value is None:
|
||||
return_value = {"state": {"dashboardId": 42}}
|
||||
extractor.client.get_dashboard_permalink_state = MagicMock(
|
||||
extractor.client.get_dashboard_permalink_state = AsyncMock(
|
||||
return_value=return_value
|
||||
)
|
||||
|
||||
@@ -188,7 +188,7 @@ class TestParseSupersetLinkDataset:
|
||||
@pytest.mark.asyncio
|
||||
async def test_dataset_detail_fetch_fails_partial_recovery(self, extractor):
|
||||
"""Dataset detail fetch failure sets partial_recovery=True."""
|
||||
extractor.client.get_dataset_detail = MagicMock(
|
||||
extractor.client.get_dataset_detail = AsyncMock(
|
||||
side_effect=Exception("Superset API timeout")
|
||||
)
|
||||
result = await extractor.parse_superset_link(self.DATASET_URL)
|
||||
|
||||
@@ -23,7 +23,7 @@ os.environ.setdefault("SECRET_KEY", "test-secret-key-for-testing")
|
||||
os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:")
|
||||
os.environ.setdefault("AUTH_DATABASE_URL", "sqlite:///:memory:")
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -117,7 +117,7 @@ class TestNormalizeImportedFilterPayload:
|
||||
class TestRecoverImportedFiltersNoDashboard:
|
||||
"""recover_imported_filters — when dashboard_id is None."""
|
||||
|
||||
def test_no_dashboard_id_url_filters_only(self, extractor):
|
||||
async def test_no_dashboard_id_url_filters_only(self, extractor):
|
||||
"""Filters from URL state are recovered without dashboard enrichment."""
|
||||
ctx = SupersetParsedContext(
|
||||
source_url="https://superset.example.com/superset/chart/42",
|
||||
@@ -134,12 +134,12 @@ class TestRecoverImportedFiltersNoDashboard:
|
||||
}
|
||||
],
|
||||
)
|
||||
result = extractor.recover_imported_filters(ctx)
|
||||
result = await extractor.recover_imported_filters(ctx)
|
||||
assert len(result) == 1
|
||||
assert result[0]["filter_name"] == "country"
|
||||
assert result[0]["raw_value"] == "US"
|
||||
|
||||
def test_empty_url_filters_returns_empty(self, extractor):
|
||||
async def test_empty_url_filters_returns_empty(self, extractor):
|
||||
"""No URL filters returns empty list when no dashboard."""
|
||||
ctx = SupersetParsedContext(
|
||||
source_url="https://superset.example.com/superset/chart/42",
|
||||
@@ -147,7 +147,7 @@ class TestRecoverImportedFiltersNoDashboard:
|
||||
chart_id=42,
|
||||
resource_type="chart",
|
||||
)
|
||||
result = extractor.recover_imported_filters(ctx)
|
||||
result = await extractor.recover_imported_filters(ctx)
|
||||
assert result == []
|
||||
|
||||
|
||||
@@ -176,9 +176,9 @@ class TestRecoverImportedFiltersWithDashboard:
|
||||
|
||||
# ── Tests ────────────────────────────────
|
||||
|
||||
def test_dashboard_recovers_native_filters(self, extractor):
|
||||
async def test_dashboard_recovers_native_filters(self, extractor):
|
||||
"""Dashboard with native filter configuration recovers filter entries."""
|
||||
extractor.client.get_dashboard = MagicMock(
|
||||
extractor.client.get_dashboard = AsyncMock(
|
||||
return_value=self._make_dashboard_payload(
|
||||
native_filter_configuration=[
|
||||
{
|
||||
@@ -197,16 +197,16 @@ class TestRecoverImportedFiltersWithDashboard:
|
||||
dashboard_id=5,
|
||||
resource_type="dashboard",
|
||||
)
|
||||
result = extractor.recover_imported_filters(ctx)
|
||||
result = await extractor.recover_imported_filters(ctx)
|
||||
assert len(result) == 1
|
||||
assert result[0]["filter_name"] == "country_filter"
|
||||
assert result[0]["display_name"] == "Country"
|
||||
assert result[0]["raw_value"] == "US"
|
||||
assert result[0]["recovery_status"] == "recovered"
|
||||
|
||||
def test_native_filter_partial_no_default(self, extractor):
|
||||
async def test_native_filter_partial_no_default(self, extractor):
|
||||
"""Native filter without default value gets partial status."""
|
||||
extractor.client.get_dashboard = MagicMock(
|
||||
extractor.client.get_dashboard = AsyncMock(
|
||||
return_value=self._make_dashboard_payload(
|
||||
native_filter_configuration=[
|
||||
{"name": "year_filter", "label": "Year"}
|
||||
@@ -219,14 +219,14 @@ class TestRecoverImportedFiltersWithDashboard:
|
||||
dataset_id=10,
|
||||
dashboard_id=5,
|
||||
)
|
||||
result = extractor.recover_imported_filters(ctx)
|
||||
result = await extractor.recover_imported_filters(ctx)
|
||||
assert len(result) == 1
|
||||
assert result[0]["recovery_status"] == "partial"
|
||||
assert result[0]["requires_confirmation"] is True
|
||||
|
||||
def test_native_filters_merged_with_url_filters(self, extractor):
|
||||
async def test_native_filters_merged_with_url_filters(self, extractor):
|
||||
"""URL-imported filters merge with dashboard metadata."""
|
||||
extractor.client.get_dashboard = MagicMock(
|
||||
extractor.client.get_dashboard = AsyncMock(
|
||||
return_value=self._make_dashboard_payload(
|
||||
native_filter_configuration=[
|
||||
{"name": "country_filter", "label": "Country", "id": "NATIVE-1"}
|
||||
@@ -247,15 +247,15 @@ class TestRecoverImportedFiltersWithDashboard:
|
||||
}
|
||||
],
|
||||
)
|
||||
result = extractor.recover_imported_filters(ctx)
|
||||
result = await extractor.recover_imported_filters(ctx)
|
||||
# Should be 1 entry after merge
|
||||
assert len(result) == 1
|
||||
# Filter name should be canonicalized from metadata
|
||||
assert result[0]["filter_name"] == "country_filter"
|
||||
|
||||
def test_default_filters_as_string_parsed(self, extractor):
|
||||
async def test_default_filters_as_string_parsed(self, extractor):
|
||||
"""default_filters JSON string is parsed."""
|
||||
extractor.client.get_dashboard = MagicMock(
|
||||
extractor.client.get_dashboard = AsyncMock(
|
||||
return_value=self._make_dashboard_payload(
|
||||
native_filter_configuration=[
|
||||
{"name": "f1", "id": "N-1"}
|
||||
@@ -269,12 +269,12 @@ class TestRecoverImportedFiltersWithDashboard:
|
||||
dataset_id=10,
|
||||
dashboard_id=5,
|
||||
)
|
||||
result = extractor.recover_imported_filters(ctx)
|
||||
result = await extractor.recover_imported_filters(ctx)
|
||||
assert result[0]["raw_value"] == "val1"
|
||||
|
||||
def test_default_filters_invalid_json_logged(self, extractor):
|
||||
async def test_default_filters_invalid_json_logged(self, extractor):
|
||||
"""Invalid default_filters JSON is caught and logged."""
|
||||
extractor.client.get_dashboard = MagicMock(
|
||||
extractor.client.get_dashboard = AsyncMock(
|
||||
return_value=self._make_dashboard_payload(
|
||||
native_filter_configuration=[
|
||||
{"name": "f1"}
|
||||
@@ -288,13 +288,13 @@ class TestRecoverImportedFiltersWithDashboard:
|
||||
dataset_id=10,
|
||||
dashboard_id=5,
|
||||
)
|
||||
result = extractor.recover_imported_filters(ctx)
|
||||
result = await extractor.recover_imported_filters(ctx)
|
||||
# Should not crash, filter has no default
|
||||
assert result[0]["raw_value"] is None
|
||||
|
||||
def test_get_dashboard_failure_falls_back(self, extractor):
|
||||
async def test_get_dashboard_failure_falls_back(self, extractor):
|
||||
"""Dashboard fetch failure falls back to URL filters only."""
|
||||
extractor.client.get_dashboard = MagicMock(
|
||||
extractor.client.get_dashboard = AsyncMock(
|
||||
side_effect=Exception("Dashboard API error")
|
||||
)
|
||||
ctx = SupersetParsedContext(
|
||||
@@ -310,13 +310,13 @@ class TestRecoverImportedFiltersWithDashboard:
|
||||
}
|
||||
],
|
||||
)
|
||||
result = extractor.recover_imported_filters(ctx)
|
||||
result = await extractor.recover_imported_filters(ctx)
|
||||
assert len(result) == 1
|
||||
assert result[0]["filter_name"] == "country"
|
||||
|
||||
def test_empty_recovered_filters_adds_fallback(self, extractor):
|
||||
async def test_empty_recovered_filters_adds_fallback(self, extractor):
|
||||
"""No recovered filters adds a fallback entry."""
|
||||
extractor.client.get_dashboard = MagicMock(
|
||||
extractor.client.get_dashboard = AsyncMock(
|
||||
return_value=self._make_dashboard_payload(
|
||||
native_filter_configuration=[],
|
||||
)
|
||||
@@ -327,14 +327,14 @@ class TestRecoverImportedFiltersWithDashboard:
|
||||
dataset_id=10,
|
||||
dashboard_id=5,
|
||||
)
|
||||
result = extractor.recover_imported_filters(ctx)
|
||||
result = await extractor.recover_imported_filters(ctx)
|
||||
assert len(result) == 1
|
||||
assert result[0]["recovery_status"] == "partial"
|
||||
assert "dashboard_5_filters" in result[0]["filter_name"]
|
||||
|
||||
def test_non_dict_item_in_native_config_skipped(self, extractor):
|
||||
async def test_non_dict_item_in_native_config_skipped(self, extractor):
|
||||
"""Non-dict items in native_filter_configuration are skipped."""
|
||||
extractor.client.get_dashboard = MagicMock(
|
||||
extractor.client.get_dashboard = AsyncMock(
|
||||
return_value=self._make_dashboard_payload(
|
||||
native_filter_configuration=[
|
||||
"not_a_dict",
|
||||
@@ -348,13 +348,13 @@ class TestRecoverImportedFiltersWithDashboard:
|
||||
dataset_id=10,
|
||||
dashboard_id=5,
|
||||
)
|
||||
result = extractor.recover_imported_filters(ctx)
|
||||
result = await extractor.recover_imported_filters(ctx)
|
||||
assert len(result) == 1
|
||||
assert result[0]["filter_name"] == "valid_filter"
|
||||
|
||||
def test_json_metadata_as_string_parsed(self, extractor):
|
||||
async def test_json_metadata_as_string_parsed(self, extractor):
|
||||
"""json_metadata as string is parsed (not dict)."""
|
||||
extractor.client.get_dashboard = MagicMock(
|
||||
extractor.client.get_dashboard = AsyncMock(
|
||||
return_value={
|
||||
"result": {
|
||||
"json_metadata": json.dumps(
|
||||
@@ -373,13 +373,13 @@ class TestRecoverImportedFiltersWithDashboard:
|
||||
dataset_id=10,
|
||||
dashboard_id=5,
|
||||
)
|
||||
result = extractor.recover_imported_filters(ctx)
|
||||
result = await extractor.recover_imported_filters(ctx)
|
||||
assert len(result) == 1
|
||||
assert result[0]["filter_name"] == "f1"
|
||||
|
||||
def test_empty_filter_name_skipped(self, extractor):
|
||||
async def test_empty_filter_name_skipped(self, extractor):
|
||||
"""Filter with empty name in native configuration is skipped."""
|
||||
extractor.client.get_dashboard = MagicMock(
|
||||
extractor.client.get_dashboard = AsyncMock(
|
||||
return_value=self._make_dashboard_payload(
|
||||
native_filter_configuration=[
|
||||
{"name": ""},
|
||||
@@ -393,7 +393,7 @@ class TestRecoverImportedFiltersWithDashboard:
|
||||
dataset_id=10,
|
||||
dashboard_id=5,
|
||||
)
|
||||
result = extractor.recover_imported_filters(ctx)
|
||||
result = await extractor.recover_imported_filters(ctx)
|
||||
assert len(result) == 1
|
||||
assert result[0]["filter_name"] == "valid_filter"
|
||||
|
||||
@@ -406,9 +406,9 @@ class TestRecoverImportedFiltersWithDashboard:
|
||||
class TestRecoveredFilterMerge:
|
||||
"""Internal merge_recovered_filter behavior via recover_imported_filters."""
|
||||
|
||||
def test_url_filter_upgraded_by_metadata(self, extractor):
|
||||
async def test_url_filter_upgraded_by_metadata(self, extractor):
|
||||
"""URL filter gets display_name from metadata when missing."""
|
||||
extractor.client.get_dashboard = MagicMock(
|
||||
extractor.client.get_dashboard = AsyncMock(
|
||||
return_value={
|
||||
"result": {
|
||||
"json_metadata": json.dumps(
|
||||
@@ -440,13 +440,13 @@ class TestRecoveredFilterMerge:
|
||||
}
|
||||
],
|
||||
)
|
||||
result = extractor.recover_imported_filters(ctx)
|
||||
result = await extractor.recover_imported_filters(ctx)
|
||||
assert result[0]["display_name"] == "Country"
|
||||
assert result[0]["filter_name"] == "country_filter"
|
||||
|
||||
def test_display_name_merged_from_metadata(self, extractor):
|
||||
async def test_display_name_merged_from_metadata(self, extractor):
|
||||
"""Existing filter without display_name gets it from metadata merge (line 60)."""
|
||||
extractor.client.get_dashboard = MagicMock(
|
||||
extractor.client.get_dashboard = AsyncMock(
|
||||
return_value={
|
||||
"result": {
|
||||
"json_metadata": json.dumps(
|
||||
@@ -475,14 +475,14 @@ class TestRecoveredFilterMerge:
|
||||
}
|
||||
],
|
||||
)
|
||||
result = extractor.recover_imported_filters(ctx)
|
||||
result = await extractor.recover_imported_filters(ctx)
|
||||
# Metadata filter with display_name should enrich the existing entry
|
||||
entry = next(f for f in result if f["filter_name"] == "country")
|
||||
assert entry["display_name"] == "Country Label"
|
||||
|
||||
def test_normalized_value_merged_from_metadata(self, extractor):
|
||||
async def test_normalized_value_merged_from_metadata(self, extractor):
|
||||
"""Existing filter without normalized_value gets it from metadata merge (line 82)."""
|
||||
extractor.client.get_dashboard = MagicMock(
|
||||
extractor.client.get_dashboard = AsyncMock(
|
||||
return_value={
|
||||
"result": {
|
||||
"json_metadata": json.dumps(
|
||||
@@ -510,15 +510,15 @@ class TestRecoveredFilterMerge:
|
||||
}
|
||||
],
|
||||
)
|
||||
result = extractor.recover_imported_filters(ctx)
|
||||
result = await extractor.recover_imported_filters(ctx)
|
||||
# Metadata filter merges — after canonicalization "N-1" becomes "status"
|
||||
status = next(f for f in result if f["filter_name"] == "status")
|
||||
# The metadata filter should have been merged with URL entry
|
||||
assert status["display_name"] is not None
|
||||
|
||||
def test_json_metadata_non_dict_fallback(self, extractor):
|
||||
async def test_json_metadata_non_dict_fallback(self, extractor):
|
||||
"""json_metadata that is not a dict is reset to empty (line 105)."""
|
||||
extractor.client.get_dashboard = MagicMock(
|
||||
extractor.client.get_dashboard = AsyncMock(
|
||||
return_value={
|
||||
"result": {
|
||||
"json_metadata": 42 # Not a dict or string
|
||||
@@ -531,7 +531,7 @@ class TestRecoveredFilterMerge:
|
||||
dataset_id=10,
|
||||
dashboard_id=5,
|
||||
)
|
||||
result = extractor.recover_imported_filters(ctx)
|
||||
result = await extractor.recover_imported_filters(ctx)
|
||||
# Should not crash, should add fallback since no native filters found
|
||||
assert len(result) == 1
|
||||
assert result[0]["recovery_status"] == "partial"
|
||||
|
||||
Reference in New Issue
Block a user