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:
@@ -6,6 +6,11 @@
|
||||
# @TEST_EDGE: target_api_timeout -> Partial success with failed_dashboards
|
||||
# @TEST_EDGE: no_selection_criteria -> Returns NO_SELECTION
|
||||
# @TEST_EDGE: zero_dashboards_match -> Returns NO_MATCHES
|
||||
# @TEST_EDGE: uninitialized_client -> Raises ValueError on falsy client
|
||||
# @TEST_EDGE: non_dict_db_mapping -> Falls back to empty dict
|
||||
# @TEST_EDGE: transform_retry_after_resolution -> Retries after wait_for_resolution
|
||||
# @TEST_EDGE: password_error_yaml_pattern -> Extracts db_name from yaml path
|
||||
# @TEST_EDGE: id_mapping_sync_failure -> Non-fatal, continues
|
||||
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock, patch, PropertyMock
|
||||
@@ -491,3 +496,235 @@ class TestMigrationPluginExecute:
|
||||
|
||||
assert result["status"] == "SUCCESS"
|
||||
assert result["mapping_count"] > 0
|
||||
|
||||
# ── Edge: uninitialized client ──
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_uninitialized_client_raises(self):
|
||||
"""Edge: SupersetClient returns falsy -> ValueError."""
|
||||
plugin = MigrationPlugin()
|
||||
src_env = _make_env("env-1", "Source")
|
||||
tgt_env = _make_env("env-2", "Target")
|
||||
|
||||
mock_cm = MagicMock()
|
||||
mock_cm.get_environments.return_value = [src_env, tgt_env]
|
||||
|
||||
with patch('src.plugins.migration.get_config_manager', return_value=mock_cm), \
|
||||
patch('src.plugins.migration.SupersetClient') as MockSC:
|
||||
|
||||
# Return None (falsy) for the source client
|
||||
MockSC.side_effect = [None, MagicMock()]
|
||||
|
||||
with pytest.raises(ValueError, match="Clients not initialized"):
|
||||
await plugin.execute({
|
||||
"from_env": "Source",
|
||||
"to_env": "Target",
|
||||
"dashboard_regex": ".*",
|
||||
})
|
||||
|
||||
# ── Edge: non-dict db_mappings ──
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_non_dict_db_mapping(self):
|
||||
"""Edge: db_mappings param is not a dict, falls back to empty dict."""
|
||||
plugin = MigrationPlugin()
|
||||
src_env = _make_env("env-1", "Source")
|
||||
tgt_env = _make_env("env-2", "Target")
|
||||
dashboards = [_make_dashboard(1, "Test Dash")]
|
||||
|
||||
mock_cm = MagicMock()
|
||||
mock_cm.get_environments.return_value = [src_env, tgt_env]
|
||||
|
||||
mock_src_client = MagicMock()
|
||||
mock_src_client.get_dashboards = MagicMock(return_value=(True, dashboards))
|
||||
mock_src_client.export_dashboard = MagicMock(return_value=(b"zip", "meta"))
|
||||
mock_tgt_client = MagicMock()
|
||||
mock_tgt_client.import_dashboard = MagicMock()
|
||||
|
||||
mock_engine = MagicMock()
|
||||
mock_engine.transform_zip.return_value = True
|
||||
|
||||
with patch('src.plugins.migration.get_config_manager', return_value=mock_cm), \
|
||||
patch('src.plugins.migration.SupersetClient') as MockSC, \
|
||||
patch('src.plugins.migration.MigrationEngine', return_value=mock_engine), \
|
||||
patch('src.plugins.migration.create_temp_file') as mock_ctf, \
|
||||
patch('src.plugins.migration.SessionLocal'):
|
||||
|
||||
MockSC.side_effect = [mock_src_client, mock_tgt_client]
|
||||
mock_ctf.return_value.__enter__ = MagicMock(return_value="/tmp/test.zip")
|
||||
|
||||
result = await plugin.execute({
|
||||
"source_env_id": "env-1",
|
||||
"target_env_id": "env-2",
|
||||
"selected_ids": [1],
|
||||
"db_mappings": "not-a-dict", # non-dict value
|
||||
})
|
||||
|
||||
assert result["status"] == "SUCCESS"
|
||||
|
||||
# ── Edge: transform fails with replace_db_config, retries ──
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_transform_retry_after_resolution(self):
|
||||
"""Edge: transform fails -> wait_for_resolution -> retry succeeds."""
|
||||
plugin = MigrationPlugin()
|
||||
src_env = _make_env("env-1", "Source")
|
||||
tgt_env = _make_env("env-2", "Target")
|
||||
|
||||
mock_cm = MagicMock()
|
||||
mock_cm.get_environments.return_value = [src_env, tgt_env]
|
||||
|
||||
mock_task_manager = MagicMock()
|
||||
mock_task_manager.wait_for_resolution = AsyncMock()
|
||||
|
||||
# Use a single client mock that handles both get_dashboards and import_dashboard
|
||||
mock_client = MagicMock()
|
||||
mock_client.get_dashboards = MagicMock(return_value=(True, [_make_dashboard(1, "Dash")]))
|
||||
mock_client.export_dashboard = MagicMock(return_value=(b"zip", "meta"))
|
||||
mock_client.import_dashboard = MagicMock()
|
||||
|
||||
mock_engine = MagicMock()
|
||||
# First call returns False (fails), second call returns True (retry succeeds)
|
||||
mock_engine.transform_zip.side_effect = [False, True]
|
||||
|
||||
# Mock DB session for Environments and DatabaseMapping
|
||||
mock_db = MagicMock()
|
||||
mock_db_env = MagicMock()
|
||||
mock_db_env.id = 1
|
||||
mock_db_env.name = "Source"
|
||||
mock_db_env2 = MagicMock()
|
||||
mock_db_env2.id = 2
|
||||
mock_db_env2.name = "Target"
|
||||
# Extend side_effect to cover initial (2) + retry (2) calls to first()
|
||||
mock_db.query.return_value.filter.return_value.first.side_effect = [
|
||||
mock_db_env, mock_db_env2, mock_db_env, mock_db_env2
|
||||
]
|
||||
mock_db.query.return_value.filter.return_value.all.return_value = []
|
||||
|
||||
with patch('src.plugins.migration.get_config_manager', return_value=mock_cm), \
|
||||
patch('src.plugins.migration.SupersetClient', return_value=mock_client), \
|
||||
patch('src.plugins.migration.MigrationEngine', return_value=mock_engine), \
|
||||
patch('src.plugins.migration.create_temp_file') as mock_ctf, \
|
||||
patch('src.dependencies.get_task_manager', return_value=mock_task_manager), \
|
||||
patch('src.plugins.migration.SessionLocal', return_value=mock_db):
|
||||
|
||||
mock_ctf.return_value.__enter__ = MagicMock(return_value="/tmp/test.zip")
|
||||
|
||||
result = await plugin.execute({
|
||||
"source_env_id": "env-1",
|
||||
"target_env_id": "env-2",
|
||||
"selected_ids": [1],
|
||||
"replace_db_config": True,
|
||||
"_task_id": "task-retry-1",
|
||||
})
|
||||
|
||||
# Production code sets PARTIAL_SUCCESS because failed_dashboards is non-empty
|
||||
# even though retry succeeds
|
||||
assert result["status"] == "PARTIAL_SUCCESS"
|
||||
assert mock_task_manager.wait_for_resolution.called
|
||||
# transform_zip should have been called twice
|
||||
assert mock_engine.transform_zip.call_count == 2
|
||||
# import_dashboard should have been called once (after retry)
|
||||
assert mock_client.import_dashboard.called
|
||||
|
||||
# ── Edge: password error with yaml path pattern ──
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_password_error_yaml_pattern(self):
|
||||
"""Edge: password error with 'databases/db_name.yaml' pattern."""
|
||||
plugin = MigrationPlugin()
|
||||
src_env = _make_env("env-1", "Source")
|
||||
tgt_env = _make_env("env-2", "Target")
|
||||
|
||||
mock_cm = MagicMock()
|
||||
mock_cm.get_environments.return_value = [src_env, tgt_env]
|
||||
|
||||
mock_task_manager = MagicMock()
|
||||
mock_task_manager.get_task.return_value = MagicMock(
|
||||
params={"passwords": {"PostgreSQL": "secret123"}}
|
||||
)
|
||||
mock_task_manager.await_input = MagicMock()
|
||||
mock_task_manager.wait_for_input = AsyncMock()
|
||||
|
||||
mock_src_client = MagicMock()
|
||||
mock_src_client.get_dashboards = MagicMock(return_value=(True, [_make_dashboard(1, "Dash")]))
|
||||
mock_src_client.export_dashboard = MagicMock(return_value=(b"zip", "meta"))
|
||||
mock_tgt_client = MagicMock()
|
||||
mock_tgt_client.import_dashboard = MagicMock(side_effect=[
|
||||
RuntimeError("Must provide a password for the database databases/PostgreSQL.yaml"),
|
||||
None,
|
||||
])
|
||||
|
||||
mock_engine = MagicMock()
|
||||
mock_engine.transform_zip.return_value = True
|
||||
|
||||
with patch('src.plugins.migration.get_config_manager', return_value=mock_cm), \
|
||||
patch('src.plugins.migration.SupersetClient') as MockSC, \
|
||||
patch('src.plugins.migration.MigrationEngine', return_value=mock_engine), \
|
||||
patch('src.plugins.migration.create_temp_file') as mock_ctf, \
|
||||
patch('src.dependencies.get_task_manager', return_value=mock_task_manager), \
|
||||
patch('src.plugins.migration.SessionLocal'):
|
||||
|
||||
MockSC.side_effect = [mock_src_client, mock_tgt_client]
|
||||
mock_ctf.return_value.__enter__ = MagicMock(return_value="/tmp/test.zip")
|
||||
|
||||
result = await plugin.execute({
|
||||
"source_env_id": "env-1",
|
||||
"target_env_id": "env-2",
|
||||
"selected_ids": [1],
|
||||
"replace_db_config": False,
|
||||
"_task_id": "task-pw-1",
|
||||
})
|
||||
|
||||
assert result["status"] == "SUCCESS"
|
||||
assert len(result["migrated_dashboards"]) == 1
|
||||
|
||||
# ── Edge: ID mapping sync failure is non-fatal ──
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_id_mapping_sync_failure(self):
|
||||
"""Edge: IdMappingService.sync_environment raises, execution continues."""
|
||||
plugin = MigrationPlugin()
|
||||
src_env = _make_env("env-1", "Source")
|
||||
tgt_env = _make_env("env-2", "Target")
|
||||
dashboards = [_make_dashboard(1, "Dash")]
|
||||
|
||||
mock_cm = MagicMock()
|
||||
mock_cm.get_environments.return_value = [src_env, tgt_env]
|
||||
|
||||
mock_src_client = MagicMock()
|
||||
mock_src_client.get_dashboards = MagicMock(return_value=(True, dashboards))
|
||||
mock_src_client.export_dashboard = MagicMock(return_value=(b"zip", "meta"))
|
||||
mock_tgt_client = MagicMock()
|
||||
mock_tgt_client.import_dashboard = MagicMock()
|
||||
|
||||
mock_engine = MagicMock()
|
||||
mock_engine.transform_zip.return_value = True
|
||||
|
||||
mock_db_session = MagicMock()
|
||||
mock_sync = MagicMock()
|
||||
mock_sync.sync_environment = MagicMock(side_effect=RuntimeError("Sync failed"))
|
||||
from src.core.mapping_service import IdMappingService
|
||||
original_sync = IdMappingService.sync_environment
|
||||
|
||||
with patch('src.plugins.migration.get_config_manager', return_value=mock_cm), \
|
||||
patch('src.plugins.migration.SupersetClient') as MockSC, \
|
||||
patch('src.plugins.migration.MigrationEngine', return_value=mock_engine), \
|
||||
patch('src.plugins.migration.create_temp_file') as mock_ctf, \
|
||||
patch('src.core.mapping_service.IdMappingService.sync_environment',
|
||||
side_effect=RuntimeError("Sync failed")), \
|
||||
patch('src.plugins.migration.SessionLocal', return_value=mock_db_session):
|
||||
|
||||
MockSC.side_effect = [mock_src_client, mock_tgt_client]
|
||||
mock_ctf.return_value.__enter__ = MagicMock(return_value="/tmp/test.zip")
|
||||
|
||||
# Should not raise — sync failure is caught and logged
|
||||
result = await plugin.execute({
|
||||
"source_env_id": "env-1",
|
||||
"target_env_id": "env-2",
|
||||
"selected_ids": [1],
|
||||
})
|
||||
|
||||
assert result["status"] == "SUCCESS"
|
||||
|
||||
# #endregion Test.MigrationPlugin
|
||||
|
||||
Reference in New Issue
Block a user