Subagents delivered tests across all uncovered backend modules: Schemas (100%): agent, auth, health, profile, settings, validation Services (98-100%): auth, profile, health, llm, mapping, resource, security, git, superset_lookup, sql_table_extractor, rbac API routes (new): auth, admin, health, environments, plugins, dashboards (helpers, projection, actions, listing), git (config, deps, env, helpers) Clean Release (100%): DTO, facade, policy_engine, stages, repos, preparation, source_isolation, compliance Git services: base, remote_providers Agent module: app, run, middleware, langgraph_setup Core: trace, cleanup, ws_log_handler, timezone, auth (config/oauth/security), matching Reports: normalizer, report_service, type_profiles Notifications: service, providers Also: - .gitignore: add .coverage, *.cover, coverage-* dirs - src/schemas/auth.py: fix AD group DN regex (comma in CN=...) - Remove co-located src/services/__tests__/ (caused pytest module collision)
256 lines
10 KiB
Python
256 lines
10 KiB
Python
# #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=[])],
|
|
)
|
|
|
|
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:
|
|
defaults.update(overrides)
|
|
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()
|
|
|
|
from src.dependencies import get_config_manager, get_db, get_resource_service, get_task_manager
|
|
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")
|
|
|
|
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
|