Группы исправлений: - Группа 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
412 lines
15 KiB
Python
412 lines
15 KiB
Python
# #region Test.Api.Migration [C:3] [TYPE Module] [SEMANTICS test,migration,routes]
|
|
# @BRIEF Tests for migration API routes — dashboards, execute, dry-run, settings, mappings, sync.
|
|
# @RELATION BINDS_TO -> [MigrationApi]
|
|
|
|
import os
|
|
|
|
os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:")
|
|
os.environ.setdefault("AUTH_DATABASE_URL", "sqlite:///:memory:")
|
|
os.environ.setdefault("SECRET_KEY", "test-secret-key-for-tests")
|
|
os.environ.setdefault("DEV_MODE", "true")
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
|
|
_src = str(Path(__file__).resolve().parent.parent.parent / "src")
|
|
if _src not in sys.path:
|
|
sys.path.insert(0, _src)
|
|
|
|
|
|
def _make_env(id_val="env-1", name="Test Env", url="https://superset.example.com"):
|
|
env = MagicMock()
|
|
env.id = id_val
|
|
env.name = name
|
|
env.url = url
|
|
return env
|
|
|
|
|
|
def _make_client(overrides: dict | None = None) -> TestClient:
|
|
from src.api.routes.migration import router
|
|
from src.core.database import get_db
|
|
from src.dependencies import get_config_manager, get_current_user, get_task_manager
|
|
from src.schemas.auth import User, RoleSchema
|
|
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
|
|
mock_user = User(
|
|
id="admin-1", username="admin", email="admin@x.com",
|
|
auth_source="LOCAL",
|
|
created_at=__import__("datetime").datetime.now(),
|
|
roles=[RoleSchema(id="r1", name="Admin", description="", permissions=[])],
|
|
)
|
|
|
|
app.dependency_overrides[get_db] = lambda: MagicMock()
|
|
app.dependency_overrides[get_current_user] = lambda: mock_user
|
|
app.dependency_overrides[get_config_manager] = lambda: MagicMock()
|
|
app.dependency_overrides[get_task_manager] = lambda: MagicMock()
|
|
if overrides:
|
|
for dep, fn in overrides.items():
|
|
app.dependency_overrides[dep] = fn
|
|
return TestClient(app)
|
|
|
|
|
|
# ── get_dashboards ──
|
|
|
|
class TestGetDashboards:
|
|
"""GET /api/environments/{env_id}/dashboards"""
|
|
|
|
def test_success(self):
|
|
mock_config = MagicMock()
|
|
mock_config.get_environments.return_value = [_make_env()]
|
|
|
|
dashboards = [{"id": 1, "title": "Test Dash", "last_modified": "2024-01-01T00:00:00", "status": "published"}]
|
|
mock_client = MagicMock()
|
|
mock_client.get_dashboards_summary = AsyncMock(return_value=dashboards)
|
|
|
|
from src.dependencies import get_config_manager
|
|
with patch("src.api.routes.migration.AsyncSupersetClient", return_value=mock_client):
|
|
client = _make_client({get_config_manager: lambda: mock_config})
|
|
resp = client.get("/api/environments/env-1/dashboards")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert len(data) == 1
|
|
assert data[0]["id"] == 1
|
|
|
|
def test_environment_not_found(self):
|
|
mock_config = MagicMock()
|
|
mock_config.get_environments.return_value = []
|
|
|
|
from src.dependencies import get_config_manager
|
|
client = _make_client({get_config_manager: lambda: mock_config})
|
|
resp = client.get("/api/environments/bad-env/dashboards")
|
|
assert resp.status_code == 404
|
|
|
|
|
|
# ── execute_migration ──
|
|
|
|
class TestExecuteMigration:
|
|
"""POST /migration/execute"""
|
|
|
|
SELECTION_PAYLOAD = {
|
|
"source_env_id": "env-1",
|
|
"target_env_id": "env-2",
|
|
"selected_ids": [1, 2, 3],
|
|
"replace_db_config": True,
|
|
"fix_cross_filters": False,
|
|
}
|
|
|
|
def test_success(self):
|
|
mock_config = MagicMock()
|
|
e1 = _make_env("env-1")
|
|
e2 = _make_env("env-2")
|
|
mock_config.get_environments.return_value = [e1, e2]
|
|
|
|
mock_tm = MagicMock()
|
|
mock_tm.create_task = AsyncMock(return_value=MagicMock(id="task-123"))
|
|
|
|
from src.dependencies import get_config_manager, get_task_manager
|
|
client = _make_client({
|
|
get_config_manager: lambda: mock_config,
|
|
get_task_manager: lambda: mock_tm,
|
|
})
|
|
resp = client.post("/api/migration/execute", json=self.SELECTION_PAYLOAD)
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["task_id"] == "task-123"
|
|
|
|
def test_invalid_source_env(self):
|
|
mock_config = MagicMock()
|
|
mock_config.get_environments.return_value = [_make_env("env-1")]
|
|
|
|
from src.dependencies import get_config_manager
|
|
client = _make_client({get_config_manager: lambda: mock_config})
|
|
resp = client.post("/api/migration/execute", json=self.SELECTION_PAYLOAD)
|
|
assert resp.status_code == 400
|
|
|
|
def test_invalid_target_env(self):
|
|
mock_config = MagicMock()
|
|
mock_config.get_environments.return_value = [_make_env("env-2")]
|
|
|
|
from src.dependencies import get_config_manager
|
|
client = _make_client({get_config_manager: lambda: mock_config})
|
|
resp = client.post("/api/migration/execute", json=self.SELECTION_PAYLOAD)
|
|
assert resp.status_code == 400
|
|
|
|
def test_invalid_target_env(self):
|
|
mock_config = MagicMock()
|
|
mock_config.get_environments.return_value = [_make_env("env-2")]
|
|
|
|
from src.dependencies import get_config_manager
|
|
client = _make_client({get_config_manager: lambda: mock_config})
|
|
resp = client.post("/api/migration/execute", json=self.SELECTION_PAYLOAD)
|
|
assert resp.status_code == 400
|
|
|
|
def test_task_creation_fails(self):
|
|
mock_config = MagicMock()
|
|
e1 = _make_env("env-1")
|
|
e2 = _make_env("env-2")
|
|
mock_config.get_environments.return_value = [e1, e2]
|
|
|
|
mock_tm = MagicMock()
|
|
mock_tm.create_task = AsyncMock(side_effect=RuntimeError("task failed"))
|
|
|
|
from src.dependencies import get_config_manager, get_task_manager
|
|
client = _make_client({
|
|
get_config_manager: lambda: mock_config,
|
|
get_task_manager: lambda: mock_tm,
|
|
})
|
|
resp = client.post("/api/migration/execute", json=self.SELECTION_PAYLOAD)
|
|
assert resp.status_code == 500
|
|
|
|
|
|
# ── dry_run_migration ──
|
|
|
|
class TestDryRunMigration:
|
|
"""POST /migration/dry-run"""
|
|
|
|
SELECTION_PAYLOAD = {
|
|
"source_env_id": "env-1",
|
|
"target_env_id": "env-2",
|
|
"selected_ids": [1, 2],
|
|
"replace_db_config": False,
|
|
"fix_cross_filters": False,
|
|
}
|
|
|
|
def test_success(self):
|
|
mock_config = MagicMock()
|
|
e1 = _make_env("env-1")
|
|
e2 = _make_env("env-2")
|
|
mock_config.get_environments.return_value = [e1, e2]
|
|
|
|
mock_dry_run = MagicMock()
|
|
mock_dry_run.run = AsyncMock(return_value={"summary": "ok", "risks": []})
|
|
|
|
from src.dependencies import get_config_manager
|
|
with patch("src.api.routes.migration.MigrationDryRunService", return_value=mock_dry_run):
|
|
client = _make_client({get_config_manager: lambda: mock_config})
|
|
resp = client.post("/api/migration/dry-run", json=self.SELECTION_PAYLOAD)
|
|
assert resp.status_code == 200
|
|
assert resp.json()["summary"] == "ok"
|
|
|
|
def test_identical_environments(self):
|
|
mock_config = MagicMock()
|
|
e1 = _make_env("env-1")
|
|
mock_config.get_environments.return_value = [e1]
|
|
|
|
from src.dependencies import get_config_manager
|
|
client = _make_client({get_config_manager: lambda: mock_config})
|
|
resp = client.post("/api/migration/dry-run", json={
|
|
"source_env_id": "env-1", "target_env_id": "env-1", "selected_ids": [1],
|
|
})
|
|
assert resp.status_code == 400
|
|
|
|
def test_invalid_environments(self):
|
|
mock_config = MagicMock()
|
|
mock_config.get_environments.return_value = []
|
|
|
|
from src.dependencies import get_config_manager
|
|
client = _make_client({get_config_manager: lambda: mock_config})
|
|
resp = client.post("/api/migration/dry-run", json=self.SELECTION_PAYLOAD)
|
|
assert resp.status_code == 400
|
|
|
|
def test_no_dashboards_selected(self):
|
|
mock_config = MagicMock()
|
|
e1 = _make_env("env-1")
|
|
e2 = _make_env("env-2")
|
|
mock_config.get_environments.return_value = [e1, e2]
|
|
|
|
from src.dependencies import get_config_manager
|
|
client = _make_client({get_config_manager: lambda: mock_config})
|
|
resp = client.post("/api/migration/dry-run", json={
|
|
"source_env_id": "env-1", "target_env_id": "env-2", "selected_ids": [],
|
|
})
|
|
assert resp.status_code == 400
|
|
|
|
def test_value_error_from_service(self):
|
|
mock_config = MagicMock()
|
|
e1 = _make_env("env-1")
|
|
e2 = _make_env("env-2")
|
|
mock_config.get_environments.return_value = [e1, e2]
|
|
|
|
mock_dry_run = MagicMock()
|
|
mock_dry_run.run = AsyncMock(side_effect=ValueError("orchestrator error"))
|
|
|
|
from src.dependencies import get_config_manager
|
|
with patch("src.api.routes.migration.MigrationDryRunService", return_value=mock_dry_run):
|
|
client = _make_client({get_config_manager: lambda: mock_config})
|
|
resp = client.post("/api/migration/dry-run", json=self.SELECTION_PAYLOAD)
|
|
assert resp.status_code == 500
|
|
|
|
|
|
# ── get_migration_settings ──
|
|
|
|
class TestGetMigrationSettings:
|
|
"""GET /migration/settings"""
|
|
|
|
def test_success(self):
|
|
mock_config = MagicMock()
|
|
mock_cfg = MagicMock()
|
|
mock_cfg.settings.migration_sync_cron = "0 2 * * *"
|
|
mock_config.get_config.return_value = mock_cfg
|
|
|
|
from src.dependencies import get_config_manager
|
|
client = _make_client({get_config_manager: lambda: mock_config})
|
|
resp = client.get("/api/migration/settings")
|
|
assert resp.status_code == 200
|
|
assert resp.json()["cron"] == "0 2 * * *"
|
|
|
|
|
|
# ── update_migration_settings ──
|
|
|
|
class TestUpdateMigrationSettings:
|
|
"""PUT /migration/settings"""
|
|
|
|
def test_success(self):
|
|
mock_config = MagicMock()
|
|
mock_cfg = MagicMock()
|
|
mock_config.get_config.return_value = mock_cfg
|
|
|
|
from src.dependencies import get_config_manager
|
|
client = _make_client({get_config_manager: lambda: mock_config})
|
|
resp = client.put("/api/migration/settings", json={"cron": "0 3 * * *"})
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["cron"] == "0 3 * * *"
|
|
assert data["status"] == "updated"
|
|
mock_config.save_config.assert_called_once()
|
|
|
|
def test_missing_cron_field(self):
|
|
mock_config = MagicMock()
|
|
from src.dependencies import get_config_manager
|
|
client = _make_client({get_config_manager: lambda: mock_config})
|
|
resp = client.put("/api/migration/settings", json={})
|
|
assert resp.status_code == 400
|
|
|
|
|
|
# ── get_resource_mappings ──
|
|
|
|
class TestGetResourceMappings:
|
|
"""GET /migration/mappings-data"""
|
|
|
|
def test_success(self):
|
|
mock_db = MagicMock()
|
|
mock_query = MagicMock()
|
|
mock_db.query.return_value = mock_query
|
|
mock_query.filter.return_value = mock_query
|
|
mock_query.count.return_value = 2
|
|
def _mock_resource_type(value: str):
|
|
m = MagicMock()
|
|
m.value = value
|
|
return m
|
|
|
|
mock_query.order_by.return_value.offset.return_value.limit.return_value.all.return_value = [
|
|
MagicMock(id=1, environment_id="env-1", resource_type=_mock_resource_type("database"), uuid="u1",
|
|
remote_integer_id=100, resource_name="DB", last_synced_at=None),
|
|
MagicMock(id=2, environment_id="env-1", resource_type=_mock_resource_type("database"), uuid="u2",
|
|
remote_integer_id=101, resource_name="DB2", last_synced_at=None),
|
|
]
|
|
|
|
from src.core.database import get_db
|
|
client = _make_client({get_db: lambda: mock_db})
|
|
resp = client.get("/api/migration/mappings-data")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["total"] == 2
|
|
assert len(data["items"]) == 2
|
|
|
|
def test_with_filters(self):
|
|
mock_db = MagicMock()
|
|
mock_query = MagicMock()
|
|
mock_db.query.return_value = mock_query
|
|
mock_query.filter.return_value = mock_query
|
|
mock_query.count.return_value = 1
|
|
mock_query.order_by.return_value.offset.return_value.limit.return_value.all.return_value = []
|
|
|
|
from src.core.database import get_db
|
|
client = _make_client({get_db: lambda: mock_db})
|
|
resp = client.get("/api/migration/mappings-data?env_id=env-1&resource_type=database&search=test&skip=0&limit=10")
|
|
assert resp.status_code == 200
|
|
|
|
|
|
# ── trigger_sync_now ──
|
|
|
|
class TestTriggerSyncNow:
|
|
"""POST /migration/sync-now"""
|
|
|
|
def test_success(self):
|
|
mock_config = MagicMock()
|
|
mock_cfg = MagicMock()
|
|
e1 = _make_env("env-1", "Env 1", "https://s1.example.com")
|
|
e2 = _make_env("env-2", "Env 2", "https://s2.example.com")
|
|
mock_cfg.environments = [e1, e2]
|
|
mock_config.get_config.return_value = mock_cfg
|
|
|
|
mock_db = MagicMock()
|
|
mock_db.query.return_value.filter_by.return_value.first.side_effect = [None, None]
|
|
|
|
mock_sync = MagicMock()
|
|
mock_sync.sync_environment = AsyncMock()
|
|
|
|
from src.core.database import get_db
|
|
from src.dependencies import get_config_manager
|
|
with patch("src.api.routes.migration.IdMappingService", return_value=mock_sync), \
|
|
patch("src.api.routes.migration.AsyncSupersetClient"), \
|
|
patch("src.api.routes.migration.get_async_job_runner"):
|
|
client = _make_client({
|
|
get_config_manager: lambda: mock_config,
|
|
get_db: lambda: mock_db,
|
|
})
|
|
resp = client.post("/api/migration/sync-now")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["status"] == "completed"
|
|
assert data["synced_count"] == 2
|
|
|
|
def test_no_environments(self):
|
|
mock_config = MagicMock()
|
|
mock_cfg = MagicMock()
|
|
mock_cfg.environments = []
|
|
mock_config.get_config.return_value = mock_cfg
|
|
|
|
from src.dependencies import get_config_manager
|
|
client = _make_client({get_config_manager: lambda: mock_config})
|
|
resp = client.post("/api/migration/sync-now")
|
|
assert resp.status_code == 400
|
|
|
|
def test_sync_with_failures(self):
|
|
mock_config = MagicMock()
|
|
mock_cfg = MagicMock()
|
|
e1 = _make_env("env-1")
|
|
e2 = _make_env("env-2")
|
|
mock_cfg.environments = [e1, e2]
|
|
mock_config.get_config.return_value = mock_cfg
|
|
|
|
mock_db = MagicMock()
|
|
mock_db.query.return_value.filter_by.return_value.first.return_value = None
|
|
|
|
mock_sync = MagicMock()
|
|
mock_sync.sync_environment = AsyncMock()
|
|
|
|
from src.core.database import get_db
|
|
from src.dependencies import get_config_manager
|
|
# Make AsyncSupersetClient raise on 2nd call to simulate sync failure
|
|
with patch("src.api.routes.migration.IdMappingService", return_value=mock_sync), \
|
|
patch("src.api.routes.migration.AsyncSupersetClient", side_effect=[MagicMock(), RuntimeError("Connection failed")]), \
|
|
patch("src.api.routes.migration.get_async_job_runner"):
|
|
client = _make_client({
|
|
get_config_manager: lambda: mock_config,
|
|
get_db: lambda: mock_db,
|
|
})
|
|
resp = client.post("/api/migration/sync-now")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["synced_count"] == 1
|
|
assert data["failed_count"] == 1
|
|
# #endregion Test.Api.Migration
|