Files
ss-tools/backend/tests/api/test_dashboard_detail_routes.py

298 lines
12 KiB
Python

# #region Test.Api.DashboardDetailRoutes [C:3] [TYPE Module] [SEMANTICS test,dashboard,detail,api]
# @BRIEF Unit tests for dashboard detail routes — db-mappings, detail, tasks history, thumbnail.
# @RELATION BINDS_TO -> [DashboardDetailRoutes]
# @TEST_EDGE: source_env_not_found -> 404
# @TEST_EDGE: target_env_not_found -> 404
# @TEST_EDGE: db_mapping_fail -> 503
# @TEST_EDGE: dashboard_not_found -> 503
# @TEST_EDGE: env_not_found_detail -> 404
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")
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_mock_env(id: str = "env-1", name: str = "TestEnv"):
env = MagicMock()
env.id = id
env.name = name
env.url = "https://superset.example.com"
env.username = "admin"
env.password = "pass"
env.verify_ssl = True
return env
def _make_client(overrides: dict | None = None) -> TestClient:
from src.api.routes.dashboards._detail_routes import router
from src.dependencies import get_config_manager, get_current_user, get_mapping_service, get_task_manager, has_permission
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=[])],
)
defaults = {
get_config_manager: lambda: MagicMock(),
get_mapping_service: lambda: MagicMock(),
get_task_manager: lambda: MagicMock(),
get_current_user: lambda: mock_user,
}
if overrides:
defaults.update(overrides)
for dep, mock_fn in defaults.items():
app.dependency_overrides[dep] = mock_fn
return TestClient(app)
# ── get_database_mappings ──
class TestGetDatabaseMappings:
"""GET /api/dashboards/db-mappings"""
def test_mappings_success(self):
mock_config = MagicMock()
mock_config.get_environments.return_value = [
_make_mock_env(id="src-env"),
_make_mock_env(id="tgt-env"),
]
mock_mapping = AsyncMock()
mock_mapping.get_suggestions.return_value = [
{"source_db": "db1", "target_db": "db2", "source_db_uuid": "u1", "target_db_uuid": "u2", "confidence": 0.95},
]
from src.dependencies import get_config_manager, get_mapping_service
client = _make_client({
get_config_manager: lambda: mock_config,
get_mapping_service: lambda: mock_mapping,
})
resp = client.get("/api/dashboards/db-mappings?source_env_id=src-env&target_env_id=tgt-env")
assert resp.status_code == 200
data = resp.json()
assert len(data["mappings"]) == 1
assert data["mappings"][0]["source_db"] == "db1"
def test_mappings_source_not_found(self):
mock_config = MagicMock()
mock_config.get_environments.return_value = [_make_mock_env(id="tgt-env")]
from src.dependencies import get_config_manager
client = _make_client({get_config_manager: lambda: mock_config})
resp = client.get("/api/dashboards/db-mappings?source_env_id=src-ghost&target_env_id=tgt-env")
assert resp.status_code == 404
assert "Source environment not found" in resp.text
def test_mappings_target_not_found(self):
mock_config = MagicMock()
mock_config.get_environments.return_value = [_make_mock_env(id="src-env")]
from src.dependencies import get_config_manager
client = _make_client({get_config_manager: lambda: mock_config})
resp = client.get("/api/dashboards/db-mappings?source_env_id=src-env&target_env_id=tgt-ghost")
assert resp.status_code == 404
assert "Target environment not found" in resp.text
def test_mappings_service_fail(self):
mock_config = MagicMock()
mock_config.get_environments.return_value = [
_make_mock_env(id="src-env"),
_make_mock_env(id="tgt-env"),
]
mock_mapping = AsyncMock()
mock_mapping.get_suggestions.side_effect = Exception("Service unavailable")
from src.dependencies import get_config_manager, get_mapping_service
client = _make_client({
get_config_manager: lambda: mock_config,
get_mapping_service: lambda: mock_mapping,
})
resp = client.get("/api/dashboards/db-mappings?source_env_id=src-env&target_env_id=tgt-env")
assert resp.status_code == 503
assert "Failed to get database mappings" in resp.text
# ── get_dashboard_detail ──
class TestGetDashboardDetail:
"""GET /api/dashboards/{dashboard_ref}"""
def test_detail_success(self):
mock_config = MagicMock()
mock_config.get_environments.return_value = [_make_mock_env(id="env-1")]
mock_client = AsyncMock()
mock_client.get_dashboards_page.return_value = (1, [{"id": 42}])
mock_client.get_dashboard_detail.return_value = {
"id": 42,
"title": "Test Dash",
"chart_count": 5,
"dataset_count": 3,
"charts": [{"id": 1, "title": "Chart 1", "viz_type": "line"}],
"datasets": [{"id": 1, "table_name": "sales", "database": "prod"}],
}
from src.dependencies import get_config_manager
with patch("src.api.routes.dashboards._detail_routes.SupersetClient", return_value=mock_client), \
patch("src.api.routes.dashboards._detail_routes.get_superset_client") as mock_reg:
mock_reg.return_value = AsyncMock()
client = _make_client({get_config_manager: lambda: mock_config})
resp = client.get("/api/dashboards/42?env_id=env-1")
assert resp.status_code == 200
data = resp.json()
assert data["id"] == 42
def test_detail_env_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/dashboards/42?env_id=env-ghost")
assert resp.status_code == 404
assert "Environment not found" in resp.text
# ── get_dashboard_tasks_history ──
class TestGetDashboardTasksHistory:
"""GET /api/dashboards/{dashboard_ref}/tasks"""
def test_tasks_history_success(self):
mock_config = MagicMock()
mock_config.get_environments.return_value = [_make_mock_env(id="env-1")]
mock_task = MagicMock()
mock_task.id = "task-1"
mock_task.plugin_id = "superset-backup"
mock_task.status = "SUCCESS"
mock_task.params = {"dashboards": ["42"], "environment_id": "env-1"}
mock_task.result = {"status": "ok", "summary": "Backup done"}
mock_task.started_at = __import__("datetime").datetime(2024, 1, 1)
mock_task.finished_at = __import__("datetime").datetime(2024, 1, 1, 0, 1)
mock_task_manager = MagicMock()
mock_task_manager.get_all_tasks.return_value = [mock_task]
from src.dependencies import get_config_manager, get_task_manager
client = _make_client({
get_config_manager: lambda: mock_config,
get_task_manager: lambda: mock_task_manager,
})
resp = client.get("/api/dashboards/42/tasks?env_id=env-1")
assert resp.status_code == 200
data = resp.json()
assert data["dashboard_id"] == 42
assert len(data["items"]) == 1
assert data["items"][0]["id"] == "task-1"
def test_tasks_history_empty(self):
mock_config = MagicMock()
mock_config.get_environments.return_value = [_make_mock_env(id="env-1")]
mock_task_manager = MagicMock()
mock_task_manager.get_all_tasks.return_value = []
from src.dependencies import get_config_manager, get_task_manager
client = _make_client({
get_config_manager: lambda: mock_config,
get_task_manager: lambda: mock_task_manager,
})
resp = client.get("/api/dashboards/42/tasks?env_id=env-1")
assert resp.status_code == 200
assert resp.json()["items"] == []
def test_tasks_history_slug_no_env_id(self):
mock_task_manager = MagicMock()
from src.dependencies import get_config_manager, get_task_manager
client = _make_client({
get_config_manager: lambda: MagicMock(),
get_task_manager: lambda: mock_task_manager,
})
resp = client.get("/api/dashboards/slug-ref/tasks")
assert resp.status_code == 400
assert "env_id is required" in resp.text
def test_tasks_history_env_not_found(self):
mock_config = MagicMock()
mock_config.get_environments.return_value = []
from src.dependencies import get_config_manager, get_task_manager
client = _make_client({
get_config_manager: lambda: mock_config,
get_task_manager: lambda: MagicMock(),
})
resp = client.get("/api/dashboards/slug-ref/tasks?env_id=env-ghost")
assert resp.status_code == 404
assert "Environment not found" in resp.text
# ── get_dashboard_thumbnail ──
class TestGetDashboardThumbnail:
"""GET /api/dashboards/{dashboard_ref}/thumbnail"""
def test_thumbnail_success(self):
mock_config = MagicMock()
mock_config.get_environments.return_value = [_make_mock_env(id="env-1")]
mock_client = AsyncMock()
mock_client.get_dashboards_page.return_value = (1, [{"id": 42}])
mock_async_client = AsyncMock()
async def _fake_request(method, endpoint, **kwargs):
if "cache_dashboard_screenshot" in endpoint:
return {"result": {"image_url": "/api/v1/dashboard/42/thumbnail/digest-1/"}}
if "raw_response" in kwargs and kwargs["raw_response"]:
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.headers = {"Content-Type": "image/png"}
mock_resp.content = b"fake-image-bytes"
return mock_resp
return {}
mock_async_client.request.side_effect = _fake_request
mock_async_client.get_dashboards_page.return_value = (1, [{"id": 42}])
from src.dependencies import get_config_manager
with patch("src.api.routes.dashboards._detail_routes.SupersetClient", return_value=mock_client), \
patch("src.api.routes.dashboards._detail_routes.get_superset_client", return_value=mock_async_client):
client = _make_client({get_config_manager: lambda: mock_config})
resp = client.get("/api/dashboards/42/thumbnail?env_id=env-1")
assert resp.status_code == 200
assert resp.content == b"fake-image-bytes"
def test_thumbnail_env_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/dashboards/42/thumbnail?env_id=env-ghost")
assert resp.status_code == 404
assert "Environment not found" in resp.text
# #endregion Test.Api.DashboardDetailRoutes