# #region Test.Api.DashboardListingRoutes [C:3] [TYPE Module] [SEMANTICS test,dashboard,listing,api] # @BRIEF Unit tests for dashboard listing route — get_dashboards. # @RELATION BINDS_TO -> [DashboardListingRoutes] # @TEST_EDGE: page_less_than_one -> 400 # @TEST_EDGE: page_size_invalid -> 400 # @TEST_EDGE: environment_not_found -> 404 # @TEST_EDGE: superset_fail -> 503 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, HTTPException 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" return env def _make_client(overrides: dict | None = None) -> TestClient: from src.api.routes.dashboards._listing_routes import router from src.core.database import get_db from src.dependencies import ( get_config_manager, get_current_user, get_resource_service, get_task_manager, has_permission, ) from src.schemas.auth import User, RoleSchema app = FastAPI() app.include_router(router) mock_user = User( id="user-1", username="testuser", email="test@example.com", auth_source="LOCAL", created_at=__import__("datetime").datetime.now(), roles=[RoleSchema(id="role-1", name="Admin", description="Admin", permissions=[])], ) # Map string keys (from fixtures) to dependency function references _DEP_KEY_MAP = { "db": get_db, "config_manager": get_config_manager, "task_manager": get_task_manager, "resource_service": get_resource_service, } defaults = { get_db: lambda: MagicMock(), get_config_manager: lambda: MagicMock(), get_task_manager: lambda: MagicMock(), get_resource_service: lambda: AsyncMock(), get_current_user: lambda: mock_user, has_permission: lambda *a, **kw: lambda: None, } if overrides: for key, value in overrides.items(): resolved_key = _DEP_KEY_MAP.get(key, key) # Wrap MagicMock/AsyncMock values so FastAPI's dependency resolution # calls the lambda and gets the configured mock, not a new instance defaults[resolved_key] = (lambda v=value: v) if isinstance(value, (MagicMock, AsyncMock)) else value for dep, mock_fn in defaults.items(): app.dependency_overrides[dep] = mock_fn return TestClient(app) class TestGetDashboards: """GET /api/dashboards""" @pytest.fixture def base_mocks(self): env = _make_mock_env("env-1") mock_config = MagicMock() mock_config.get_environments.return_value = [env] mock_task_manager = MagicMock() mock_task_manager.get_all_tasks.return_value = [] mock_rs = AsyncMock() mock_rs.get_dashboards_page_with_status.return_value = { "dashboards": [ {"id": 1, "title": "Main Dashboard", "slug": "main", "owners": []}, ], "total": 1, "total_pages": 1, } mock_db = MagicMock() return { "config_manager": mock_config, "task_manager": mock_task_manager, "resource_service": mock_rs, "db": mock_db, } def test_get_dashboards_success(self, base_mocks): """Happy path: returns paginated dashboards.""" client = _make_client(base_mocks) resp = client.get("/api/dashboards?env_id=env-1") assert resp.status_code == 200 data = resp.json() assert data["total"] == 1 assert len(data["dashboards"]) == 1 assert data["dashboards"][0]["title"] == "Main Dashboard" def test_get_dashboards_page_less_than_one(self, base_mocks): """Page < 1 returns 400.""" client = _make_client(base_mocks) resp = client.get("/api/dashboards?env_id=env-1&page=0") assert resp.status_code == 400 assert "Page must be >= 1" in resp.text def test_get_dashboards_page_size_invalid(self, base_mocks): """page_size < 1 returns 400.""" client = _make_client(base_mocks) resp = client.get("/api/dashboards?env_id=env-1&page_size=0") assert resp.status_code == 400 def test_get_dashboards_page_size_too_large(self, base_mocks): """page_size > 100 returns 400.""" client = _make_client(base_mocks) resp = client.get("/api/dashboards?env_id=env-1&page_size=101") assert resp.status_code == 400 assert "Page size must be between 1 and 100" in resp.text def test_get_dashboards_env_not_found(self, base_mocks): """Non-existent env returns 404.""" mock_config = MagicMock() mock_config.get_environments.return_value = [] mocks = {**base_mocks, "config_manager": mock_config} client = _make_client(mocks) resp = client.get("/api/dashboards?env_id=env-ghost") assert resp.status_code == 404 assert "Environment not found" in resp.text def test_get_dashboards_with_search(self, base_mocks): """Search filter applied via fallback path.""" mock_rs = base_mocks["resource_service"] mock_rs.get_dashboards_page_with_status.side_effect = Exception("No page support") mock_rs.get_dashboards_with_status.return_value = [ {"id": 1, "title": "Revenue Dashboard", "slug": "revenue", "owners": []}, {"id": 2, "title": "Sales Report", "slug": "sales", "owners": []}, ] client = _make_client(base_mocks) resp = client.get("/api/dashboards?env_id=env-1&search=revenue") assert resp.status_code == 200 data = resp.json() assert data["total"] == 1 assert data["dashboards"][0]["title"] == "Revenue Dashboard" def test_get_dashboards_full_scan_with_filters(self, base_mocks): """Column filters trigger full scan path.""" mock_rs = base_mocks["resource_service"] mock_rs.get_dashboards_page_with_status.side_effect = Exception("No page support") mock_rs.get_dashboards_with_status.return_value = [ {"id": 1, "title": "Main Dashboard", "slug": "main", "owners": [], "git_status": {"sync_status": "OK", "has_repo": True}}, ] client = _make_client(base_mocks) resp = client.get("/api/dashboards?env_id=env-1&filter_git_status=ok") assert resp.status_code == 200 data = resp.json() assert data["total"] == 1 def test_get_dashboards_internal_exception(self, base_mocks): """Internal error returns 503.""" mock_rs = base_mocks["resource_service"] mock_rs.get_dashboards_page_with_status.side_effect = Exception("Critical failure") # Also make the fallback fail to trigger the outer 503 handler mock_rs.get_dashboards_with_status.side_effect = Exception("Fallback also fails") client = _make_client(base_mocks) resp = client.get("/api/dashboards?env_id=env-1") assert resp.status_code == 503 assert "Failed to fetch dashboards" in resp.text def test_get_dashboards_slug_filter_only(self, base_mocks): """Slug-only filter.""" mock_rs = base_mocks["resource_service"] mock_rs.get_dashboards_page_with_status.side_effect = Exception("No page support") mock_rs.get_dashboards_with_status.return_value = [ {"id": 1, "title": "Main", "slug": "", "owners": []}, {"id": 2, "title": "With Slug", "slug": "with-slug", "owners": []}, ] client = _make_client(base_mocks) resp = client.get("/api/dashboards?env_id=env-1&page_context=dashboards_main&apply_profile_default=false&override_show_all=true") assert resp.status_code == 200 def test_get_dashboards_title_filters(self, base_mocks): """Title column filter applied.""" mock_rs = base_mocks["resource_service"] mock_rs.get_dashboards_page_with_status.side_effect = Exception("No page support") mock_rs.get_dashboards_with_status.return_value = [ {"id": 1, "title": "Revenue Dashboard", "slug": "revenue", "owners": []}, {"id": 2, "title": "Sales Dashboard", "slug": "sales", "owners": []}, ] client = _make_client(base_mocks) resp = client.get("/api/dashboards?env_id=env-1&filter_title=revenue+dashboard") assert resp.status_code == 200 data = resp.json() assert data["total"] == 1 def test_get_dashboards_actor_filter(self, base_mocks): """Actor column filter applied.""" mock_rs = base_mocks["resource_service"] mock_rs.get_dashboards_page_with_status.side_effect = Exception("No page support") mock_rs.get_dashboards_with_status.return_value = [ {"id": 1, "title": "Main", "slug": "main", "owners": [], "last_modified": "2024-01-01"}, ] client = _make_client(base_mocks) resp = client.get("/api/dashboards?env_id=env-1&filter_actor=alice") assert resp.status_code == 200 def test_get_dashboards_changed_on_filter(self, base_mocks): """Changed-on filter applied.""" mock_rs = base_mocks["resource_service"] mock_rs.get_dashboards_page_with_status.side_effect = Exception("No page support") mock_rs.get_dashboards_with_status.return_value = [ {"id": 1, "title": "Main", "slug": "main", "owners": [], "last_modified": "2024-01-01T12:00:00"}, ] client = _make_client(base_mocks) resp = client.get("/api/dashboards?env_id=env-1&filter_changed_on=2024-01-01") assert resp.status_code == 200 def test_get_dashboards_page_fallback_to_full_scan(self, base_mocks): """When page-based fetch fails, fallback to full scan.""" mock_rs = base_mocks["resource_service"] mock_rs.get_dashboards_page_with_status.side_effect = Exception("No page support") mock_rs.get_dashboards_with_status.return_value = [ {"id": 1, "title": "First", "slug": "first", "owners": []}, {"id": 2, "title": "Second", "slug": "second", "owners": []}, ] client = _make_client(base_mocks) resp = client.get("/api/dashboards?env_id=env-1&page=1&page_size=1") assert resp.status_code == 200 data = resp.json() assert data["total"] == 2 assert data["total_pages"] == 2 assert len(data["dashboards"]) == 1 # #endregion Test.Api.DashboardListingRoutes