878 lines
34 KiB
Python
878 lines
34 KiB
Python
# [DEF:backend.src.api.routes.__tests__.test_dashboards:Module]
|
|
# @TIER: STANDARD
|
|
# @PURPOSE: Unit tests for Dashboards API endpoints
|
|
# @LAYER: API
|
|
# @RELATION: TESTS -> backend.src.api.routes.dashboards
|
|
|
|
import pytest
|
|
from unittest.mock import MagicMock, patch, AsyncMock
|
|
from datetime import datetime, timezone
|
|
from fastapi.testclient import TestClient
|
|
from src.app import app
|
|
from src.api.routes.dashboards import DashboardsResponse
|
|
from src.dependencies import get_current_user, has_permission, get_config_manager, get_task_manager, get_resource_service, get_mapping_service
|
|
from src.core.database import get_db
|
|
from src.services.profile_service import ProfileService as DomainProfileService
|
|
|
|
# Global mock user for get_current_user dependency overrides
|
|
mock_user = MagicMock()
|
|
mock_user.id = "u-1"
|
|
mock_user.username = "testuser"
|
|
mock_user.roles = []
|
|
admin_role = MagicMock()
|
|
admin_role.name = "Admin"
|
|
mock_user.roles.append(admin_role)
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def mock_deps():
|
|
config_manager = MagicMock()
|
|
task_manager = MagicMock()
|
|
resource_service = MagicMock()
|
|
mapping_service = MagicMock()
|
|
|
|
db = MagicMock()
|
|
|
|
app.dependency_overrides[get_config_manager] = lambda: config_manager
|
|
app.dependency_overrides[get_task_manager] = lambda: task_manager
|
|
app.dependency_overrides[get_resource_service] = lambda: resource_service
|
|
app.dependency_overrides[get_mapping_service] = lambda: mapping_service
|
|
app.dependency_overrides[get_current_user] = lambda: mock_user
|
|
app.dependency_overrides[get_db] = lambda: db
|
|
|
|
app.dependency_overrides[has_permission("plugin:migration", "READ")] = lambda: mock_user
|
|
app.dependency_overrides[has_permission("plugin:migration", "EXECUTE")] = lambda: mock_user
|
|
app.dependency_overrides[has_permission("plugin:backup", "EXECUTE")] = lambda: mock_user
|
|
app.dependency_overrides[has_permission("tasks", "READ")] = lambda: mock_user
|
|
|
|
yield {
|
|
"config": config_manager,
|
|
"task": task_manager,
|
|
"resource": resource_service,
|
|
"mapping": mapping_service,
|
|
"db": db,
|
|
}
|
|
app.dependency_overrides.clear()
|
|
|
|
client = TestClient(app)
|
|
|
|
|
|
# [DEF:test_get_dashboards_success:Function]
|
|
# @TEST: GET /api/dashboards returns 200 and valid schema
|
|
# @PRE: env_id exists
|
|
# @POST: Response matches DashboardsResponse schema
|
|
def test_get_dashboards_success(mock_deps):
|
|
"""Uses @TEST_FIXTURE: dashboard_list_happy data."""
|
|
mock_env = MagicMock()
|
|
mock_env.id = "prod"
|
|
mock_deps["config"].get_environments.return_value = [mock_env]
|
|
mock_deps["task"].get_all_tasks.return_value = []
|
|
|
|
# @TEST_FIXTURE: dashboard_list_happy -> {"id": 1, "title": "Main Revenue"}
|
|
mock_deps["resource"].get_dashboards_with_status = AsyncMock(return_value=[
|
|
{
|
|
"id": 1,
|
|
"title": "Main Revenue",
|
|
"slug": "main-revenue",
|
|
"git_status": {"branch": "main", "sync_status": "OK"},
|
|
"last_task": {"task_id": "task-1", "status": "SUCCESS"}
|
|
}
|
|
])
|
|
|
|
response = client.get("/api/dashboards?env_id=prod")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
# exhaustive @POST assertions
|
|
assert "dashboards" in data
|
|
assert len(data["dashboards"]) == 1
|
|
assert data["dashboards"][0]["title"] == "Main Revenue"
|
|
assert data["total"] == 1
|
|
assert "page" in data
|
|
DashboardsResponse(**data)
|
|
|
|
|
|
# [/DEF:test_get_dashboards_success:Function]
|
|
|
|
|
|
# [DEF:test_get_dashboards_with_search:Function]
|
|
# @TEST: GET /api/dashboards filters by search term
|
|
# @PRE: search parameter provided
|
|
# @POST: Only matching dashboards returned
|
|
def test_get_dashboards_with_search(mock_deps):
|
|
mock_env = MagicMock()
|
|
mock_env.id = "prod"
|
|
mock_deps["config"].get_environments.return_value = [mock_env]
|
|
mock_deps["task"].get_all_tasks.return_value = []
|
|
|
|
async def mock_get_dashboards(env, tasks, include_git_status=False):
|
|
return [
|
|
{"id": 1, "title": "Sales Report", "slug": "sales", "git_status": {"branch": "main", "sync_status": "OK"}, "last_task": None},
|
|
{"id": 2, "title": "Marketing Dashboard", "slug": "marketing", "git_status": {"branch": "main", "sync_status": "OK"}, "last_task": None}
|
|
]
|
|
mock_deps["resource"].get_dashboards_with_status = AsyncMock(
|
|
side_effect=mock_get_dashboards
|
|
)
|
|
|
|
response = client.get("/api/dashboards?env_id=prod&search=sales")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
# @POST: Filtered result count must match search
|
|
assert len(data["dashboards"]) == 1
|
|
assert data["dashboards"][0]["title"] == "Sales Report"
|
|
|
|
|
|
# [/DEF:test_get_dashboards_with_search:Function]
|
|
|
|
|
|
# [DEF:test_get_dashboards_empty:Function]
|
|
# @TEST_EDGE: empty_dashboards -> {env_id: 'empty_env', expected_total: 0}
|
|
def test_get_dashboards_empty(mock_deps):
|
|
"""@TEST_EDGE: empty_dashboards -> {env_id: 'empty_env', expected_total: 0}"""
|
|
mock_env = MagicMock()
|
|
mock_env.id = "empty_env"
|
|
mock_deps["config"].get_environments.return_value = [mock_env]
|
|
mock_deps["task"].get_all_tasks.return_value = []
|
|
mock_deps["resource"].get_dashboards_with_status = AsyncMock(return_value=[])
|
|
|
|
response = client.get("/api/dashboards?env_id=empty_env")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["total"] == 0
|
|
assert len(data["dashboards"]) == 0
|
|
assert data["total_pages"] == 1
|
|
DashboardsResponse(**data)
|
|
# [/DEF:test_get_dashboards_empty:Function]
|
|
|
|
|
|
# [DEF:test_get_dashboards_superset_failure:Function]
|
|
# @TEST_EDGE: external_superset_failure -> {env_id: 'bad_conn', status: 503}
|
|
def test_get_dashboards_superset_failure(mock_deps):
|
|
"""@TEST_EDGE: external_superset_failure -> {env_id: 'bad_conn', status: 503}"""
|
|
mock_env = MagicMock()
|
|
mock_env.id = "bad_conn"
|
|
mock_deps["config"].get_environments.return_value = [mock_env]
|
|
mock_deps["task"].get_all_tasks.return_value = []
|
|
mock_deps["resource"].get_dashboards_with_status = AsyncMock(
|
|
side_effect=Exception("Connection refused")
|
|
)
|
|
|
|
response = client.get("/api/dashboards?env_id=bad_conn")
|
|
assert response.status_code == 503
|
|
assert "Failed to fetch dashboards" in response.json()["detail"]
|
|
# [/DEF:test_get_dashboards_superset_failure:Function]
|
|
|
|
|
|
# [DEF:test_get_dashboards_env_not_found:Function]
|
|
# @TEST: GET /api/dashboards returns 404 if env_id missing
|
|
# @PRE: env_id does not exist
|
|
# @POST: Returns 404 error
|
|
def test_get_dashboards_env_not_found(mock_deps):
|
|
mock_deps["config"].get_environments.return_value = []
|
|
response = client.get("/api/dashboards?env_id=nonexistent")
|
|
|
|
assert response.status_code == 404
|
|
assert "Environment not found" in response.json()["detail"]
|
|
|
|
|
|
# [/DEF:test_get_dashboards_env_not_found:Function]
|
|
|
|
|
|
# [DEF:test_get_dashboards_invalid_pagination:Function]
|
|
# @TEST: GET /api/dashboards returns 400 for invalid page/page_size
|
|
# @PRE: page < 1 or page_size > 100
|
|
# @POST: Returns 400 error
|
|
def test_get_dashboards_invalid_pagination(mock_deps):
|
|
mock_env = MagicMock()
|
|
mock_env.id = "prod"
|
|
mock_deps["config"].get_environments.return_value = [mock_env]
|
|
# Invalid page
|
|
response = client.get("/api/dashboards?env_id=prod&page=0")
|
|
assert response.status_code == 400
|
|
assert "Page must be >= 1" in response.json()["detail"]
|
|
|
|
# Invalid page_size
|
|
response = client.get("/api/dashboards?env_id=prod&page_size=101")
|
|
assert response.status_code == 400
|
|
assert "Page size must be between 1 and 100" in response.json()["detail"]
|
|
# [/DEF:test_get_dashboards_invalid_pagination:Function]
|
|
|
|
|
|
# [DEF:test_get_dashboard_detail_success:Function]
|
|
# @TEST: GET /api/dashboards/{id} returns dashboard detail with charts and datasets
|
|
def test_get_dashboard_detail_success(mock_deps):
|
|
with patch("src.api.routes.dashboards.SupersetClient") as mock_client_cls:
|
|
mock_env = MagicMock()
|
|
mock_env.id = "prod"
|
|
mock_deps["config"].get_environments.return_value = [mock_env]
|
|
|
|
mock_client = MagicMock()
|
|
mock_client.get_dashboard_detail.return_value = {
|
|
"id": 42,
|
|
"title": "Revenue Dashboard",
|
|
"slug": "revenue-dashboard",
|
|
"url": "/superset/dashboard/42/",
|
|
"description": "Overview",
|
|
"last_modified": "2026-02-20T10:00:00+00:00",
|
|
"published": True,
|
|
"charts": [
|
|
{
|
|
"id": 100,
|
|
"title": "Revenue by Month",
|
|
"viz_type": "line",
|
|
"dataset_id": 7,
|
|
"last_modified": "2026-02-19T10:00:00+00:00",
|
|
"overview": "line"
|
|
}
|
|
],
|
|
"datasets": [
|
|
{
|
|
"id": 7,
|
|
"table_name": "fact_revenue",
|
|
"schema": "mart",
|
|
"database": "Analytics",
|
|
"last_modified": "2026-02-18T10:00:00+00:00",
|
|
"overview": "mart.fact_revenue"
|
|
}
|
|
],
|
|
"chart_count": 1,
|
|
"dataset_count": 1
|
|
}
|
|
mock_client_cls.return_value = mock_client
|
|
|
|
response = client.get("/api/dashboards/42?env_id=prod")
|
|
|
|
assert response.status_code == 200
|
|
payload = response.json()
|
|
assert payload["id"] == 42
|
|
assert payload["chart_count"] == 1
|
|
assert payload["dataset_count"] == 1
|
|
# [/DEF:test_get_dashboard_detail_success:Function]
|
|
|
|
|
|
# [DEF:test_get_dashboard_detail_env_not_found:Function]
|
|
# @TEST: GET /api/dashboards/{id} returns 404 for missing environment
|
|
def test_get_dashboard_detail_env_not_found(mock_deps):
|
|
mock_deps["config"].get_environments.return_value = []
|
|
|
|
response = client.get("/api/dashboards/42?env_id=missing")
|
|
|
|
assert response.status_code == 404
|
|
assert "Environment not found" in response.json()["detail"]
|
|
# [/DEF:test_get_dashboard_detail_env_not_found:Function]
|
|
|
|
|
|
# [DEF:test_migrate_dashboards_success:Function]
|
|
# @TEST: POST /api/dashboards/migrate creates migration task
|
|
# @PRE: Valid source_env_id, target_env_id, dashboard_ids
|
|
# @POST: Returns task_id and create_task was called
|
|
def test_migrate_dashboards_success(mock_deps):
|
|
mock_source = MagicMock()
|
|
mock_source.id = "source"
|
|
mock_target = MagicMock()
|
|
mock_target.id = "target"
|
|
mock_deps["config"].get_environments.return_value = [mock_source, mock_target]
|
|
|
|
mock_task = MagicMock()
|
|
mock_task.id = "task-migrate-123"
|
|
mock_deps["task"].create_task = AsyncMock(return_value=mock_task)
|
|
|
|
response = client.post(
|
|
"/api/dashboards/migrate",
|
|
json={
|
|
"source_env_id": "source",
|
|
"target_env_id": "target",
|
|
"dashboard_ids": [1, 2, 3],
|
|
"db_mappings": {"old_db": "new_db"}
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "task_id" in data
|
|
# @POST/@SIDE_EFFECT: create_task was called
|
|
mock_deps["task"].create_task.assert_called_once()
|
|
|
|
|
|
# [/DEF:test_migrate_dashboards_success:Function]
|
|
|
|
|
|
# [DEF:test_migrate_dashboards_no_ids:Function]
|
|
# @TEST: POST /api/dashboards/migrate returns 400 for empty dashboard_ids
|
|
# @PRE: dashboard_ids is empty
|
|
# @POST: Returns 400 error
|
|
def test_migrate_dashboards_no_ids(mock_deps):
|
|
response = client.post(
|
|
"/api/dashboards/migrate",
|
|
json={
|
|
"source_env_id": "source",
|
|
"target_env_id": "target",
|
|
"dashboard_ids": []
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 400
|
|
assert "At least one dashboard ID must be provided" in response.json()["detail"]
|
|
|
|
|
|
# [/DEF:test_migrate_dashboards_no_ids:Function]
|
|
|
|
|
|
# [DEF:test_migrate_dashboards_env_not_found:Function]
|
|
# @PRE: source_env_id and target_env_id are valid environment IDs
|
|
def test_migrate_dashboards_env_not_found(mock_deps):
|
|
"""@PRE: source_env_id and target_env_id are valid environment IDs."""
|
|
mock_deps["config"].get_environments.return_value = []
|
|
response = client.post(
|
|
"/api/dashboards/migrate",
|
|
json={
|
|
"source_env_id": "ghost",
|
|
"target_env_id": "t",
|
|
"dashboard_ids": [1]
|
|
}
|
|
)
|
|
assert response.status_code == 404
|
|
assert "Source environment not found" in response.json()["detail"]
|
|
# [/DEF:test_migrate_dashboards_env_not_found:Function]
|
|
|
|
|
|
# [DEF:test_backup_dashboards_success:Function]
|
|
# @TEST: POST /api/dashboards/backup creates backup task
|
|
# @PRE: Valid env_id, dashboard_ids
|
|
# @POST: Returns task_id and create_task was called
|
|
def test_backup_dashboards_success(mock_deps):
|
|
mock_env = MagicMock()
|
|
mock_env.id = "prod"
|
|
mock_deps["config"].get_environments.return_value = [mock_env]
|
|
|
|
mock_task = MagicMock()
|
|
mock_task.id = "task-backup-456"
|
|
mock_deps["task"].create_task = AsyncMock(return_value=mock_task)
|
|
|
|
response = client.post(
|
|
"/api/dashboards/backup",
|
|
json={
|
|
"env_id": "prod",
|
|
"dashboard_ids": [1, 2, 3],
|
|
"schedule": "0 0 * * *"
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "task_id" in data
|
|
# @POST/@SIDE_EFFECT: create_task was called
|
|
mock_deps["task"].create_task.assert_called_once()
|
|
|
|
|
|
# [/DEF:test_backup_dashboards_success:Function]
|
|
|
|
|
|
# [DEF:test_backup_dashboards_env_not_found:Function]
|
|
# @PRE: env_id is a valid environment ID
|
|
def test_backup_dashboards_env_not_found(mock_deps):
|
|
"""@PRE: env_id is a valid environment ID."""
|
|
mock_deps["config"].get_environments.return_value = []
|
|
response = client.post(
|
|
"/api/dashboards/backup",
|
|
json={
|
|
"env_id": "ghost",
|
|
"dashboard_ids": [1]
|
|
}
|
|
)
|
|
assert response.status_code == 404
|
|
assert "Environment not found" in response.json()["detail"]
|
|
# [/DEF:test_backup_dashboards_env_not_found:Function]
|
|
|
|
|
|
# [DEF:test_get_database_mappings_success:Function]
|
|
# @TEST: GET /api/dashboards/db-mappings returns mapping suggestions
|
|
# @PRE: Valid source_env_id, target_env_id
|
|
# @POST: Returns list of database mappings
|
|
def test_get_database_mappings_success(mock_deps):
|
|
mock_source = MagicMock()
|
|
mock_source.id = "prod"
|
|
mock_target = MagicMock()
|
|
mock_target.id = "staging"
|
|
mock_deps["config"].get_environments.return_value = [mock_source, mock_target]
|
|
|
|
mock_deps["mapping"].get_suggestions = AsyncMock(return_value=[
|
|
{
|
|
"source_db": "old_sales",
|
|
"target_db": "new_sales",
|
|
"source_db_uuid": "uuid-1",
|
|
"target_db_uuid": "uuid-2",
|
|
"confidence": 0.95
|
|
}
|
|
])
|
|
|
|
response = client.get("/api/dashboards/db-mappings?source_env_id=prod&target_env_id=staging")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "mappings" in data
|
|
assert len(data["mappings"]) == 1
|
|
assert data["mappings"][0]["confidence"] == 0.95
|
|
|
|
|
|
# [/DEF:test_get_database_mappings_success:Function]
|
|
|
|
|
|
# [DEF:test_get_database_mappings_env_not_found:Function]
|
|
# @PRE: source_env_id and target_env_id are valid environment IDs
|
|
def test_get_database_mappings_env_not_found(mock_deps):
|
|
"""@PRE: source_env_id must be a valid environment."""
|
|
mock_deps["config"].get_environments.return_value = []
|
|
response = client.get("/api/dashboards/db-mappings?source_env_id=ghost&target_env_id=t")
|
|
assert response.status_code == 404
|
|
# [/DEF:test_get_database_mappings_env_not_found:Function]
|
|
|
|
|
|
# [DEF:test_get_dashboard_tasks_history_filters_success:Function]
|
|
# @TEST: GET /api/dashboards/{id}/tasks returns backup and llm tasks for dashboard
|
|
def test_get_dashboard_tasks_history_filters_success(mock_deps):
|
|
now = datetime.now(timezone.utc)
|
|
|
|
llm_task = MagicMock()
|
|
llm_task.id = "task-llm-1"
|
|
llm_task.plugin_id = "llm_dashboard_validation"
|
|
llm_task.status = "SUCCESS"
|
|
llm_task.started_at = now
|
|
llm_task.finished_at = now
|
|
llm_task.params = {"dashboard_id": "42", "environment_id": "prod"}
|
|
llm_task.result = {"summary": "LLM validation complete"}
|
|
|
|
backup_task = MagicMock()
|
|
backup_task.id = "task-backup-1"
|
|
backup_task.plugin_id = "superset-backup"
|
|
backup_task.status = "RUNNING"
|
|
backup_task.started_at = now
|
|
backup_task.finished_at = None
|
|
backup_task.params = {"env": "prod", "dashboards": [42]}
|
|
backup_task.result = {}
|
|
|
|
other_task = MagicMock()
|
|
other_task.id = "task-other"
|
|
other_task.plugin_id = "superset-backup"
|
|
other_task.status = "SUCCESS"
|
|
other_task.started_at = now
|
|
other_task.finished_at = now
|
|
other_task.params = {"env": "prod", "dashboards": [777]}
|
|
other_task.result = {}
|
|
|
|
mock_deps["task"].get_all_tasks.return_value = [other_task, llm_task, backup_task]
|
|
|
|
response = client.get("/api/dashboards/42/tasks?env_id=prod&limit=10")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["dashboard_id"] == 42
|
|
assert len(data["items"]) == 2
|
|
assert {item["plugin_id"] for item in data["items"]} == {"llm_dashboard_validation", "superset-backup"}
|
|
# [/DEF:test_get_dashboard_tasks_history_filters_success:Function]
|
|
|
|
|
|
# [DEF:test_get_dashboard_thumbnail_success:Function]
|
|
# @TEST: GET /api/dashboards/{id}/thumbnail proxies image bytes from Superset
|
|
def test_get_dashboard_thumbnail_success(mock_deps):
|
|
with patch("src.api.routes.dashboards.SupersetClient") as mock_client_cls:
|
|
mock_env = MagicMock()
|
|
mock_env.id = "prod"
|
|
mock_deps["config"].get_environments.return_value = [mock_env]
|
|
|
|
mock_client = MagicMock()
|
|
mock_response = MagicMock()
|
|
mock_response.status_code = 200
|
|
mock_response.content = b"fake-image-bytes"
|
|
mock_response.headers = {"Content-Type": "image/png"}
|
|
|
|
def _network_request(method, endpoint, **kwargs):
|
|
if method == "POST":
|
|
return {"image_url": "/api/v1/dashboard/42/screenshot/abc123/"}
|
|
return mock_response
|
|
|
|
mock_client.network.request.side_effect = _network_request
|
|
mock_client_cls.return_value = mock_client
|
|
|
|
response = client.get("/api/dashboards/42/thumbnail?env_id=prod")
|
|
|
|
assert response.status_code == 200
|
|
assert response.content == b"fake-image-bytes"
|
|
assert response.headers["content-type"].startswith("image/png")
|
|
# [/DEF:test_get_dashboard_thumbnail_success:Function]
|
|
|
|
|
|
# [DEF:_build_profile_preference_stub:Function]
|
|
# @PURPOSE: Creates profile preference payload stub for dashboards filter contract tests.
|
|
# @PRE: username can be empty; enabled indicates profile-default toggle state.
|
|
# @POST: Returns object compatible with ProfileService.get_my_preference contract.
|
|
def _build_profile_preference_stub(username: str, enabled: bool):
|
|
preference = MagicMock()
|
|
preference.superset_username = username
|
|
preference.superset_username_normalized = str(username or "").strip().lower() or None
|
|
preference.show_only_my_dashboards = bool(enabled)
|
|
|
|
payload = MagicMock()
|
|
payload.preference = preference
|
|
return payload
|
|
# [/DEF:_build_profile_preference_stub:Function]
|
|
|
|
|
|
# [DEF:_matches_actor_case_insensitive:Function]
|
|
# @PURPOSE: Applies trim + case-insensitive owners OR modified_by matching used by route contract tests.
|
|
# @PRE: owners can be None or list-like values.
|
|
# @POST: Returns True when bound username matches any owner or modified_by.
|
|
def _matches_actor_case_insensitive(bound_username, owners, modified_by):
|
|
normalized_bound = str(bound_username or "").strip().lower()
|
|
if not normalized_bound:
|
|
return False
|
|
|
|
owner_tokens = []
|
|
for owner in owners or []:
|
|
token = str(owner or "").strip().lower()
|
|
if token:
|
|
owner_tokens.append(token)
|
|
|
|
modified_token = str(modified_by or "").strip().lower()
|
|
return normalized_bound in owner_tokens or bool(modified_token and modified_token == normalized_bound)
|
|
# [/DEF:_matches_actor_case_insensitive:Function]
|
|
|
|
|
|
# [DEF:test_get_dashboards_profile_filter_contract_owners_or_modified_by:Function]
|
|
# @TEST: GET /api/dashboards applies profile-default filter with owners OR modified_by trim+case-insensitive semantics.
|
|
# @PRE: Current user has enabled profile-default preference and bound username.
|
|
# @POST: Response includes only matching dashboards and effective_profile_filter metadata.
|
|
def test_get_dashboards_profile_filter_contract_owners_or_modified_by(mock_deps):
|
|
mock_env = MagicMock()
|
|
mock_env.id = "prod"
|
|
mock_deps["config"].get_environments.return_value = [mock_env]
|
|
mock_deps["task"].get_all_tasks.return_value = []
|
|
mock_deps["resource"].get_dashboards_with_status = AsyncMock(return_value=[
|
|
{
|
|
"id": 1,
|
|
"title": "Owner Match",
|
|
"slug": "owner-match",
|
|
"owners": [" John_Doe "],
|
|
"modified_by": "someone_else",
|
|
},
|
|
{
|
|
"id": 2,
|
|
"title": "Modifier Match",
|
|
"slug": "modifier-match",
|
|
"owners": ["analytics-team"],
|
|
"modified_by": " JOHN_DOE ",
|
|
},
|
|
{
|
|
"id": 3,
|
|
"title": "No Match",
|
|
"slug": "no-match",
|
|
"owners": ["another-user"],
|
|
"modified_by": "nobody",
|
|
},
|
|
])
|
|
|
|
with patch("src.api.routes.dashboards.ProfileService") as profile_service_cls:
|
|
profile_service = MagicMock()
|
|
profile_service.get_my_preference.return_value = _build_profile_preference_stub(
|
|
username=" JOHN_DOE ",
|
|
enabled=True,
|
|
)
|
|
profile_service.matches_dashboard_actor.side_effect = _matches_actor_case_insensitive
|
|
profile_service_cls.return_value = profile_service
|
|
|
|
response = client.get(
|
|
"/api/dashboards?env_id=prod&page_context=dashboards_main&apply_profile_default=true"
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
payload = response.json()
|
|
|
|
assert payload["total"] == 2
|
|
assert {item["id"] for item in payload["dashboards"]} == {1, 2}
|
|
assert payload["effective_profile_filter"]["applied"] is True
|
|
assert payload["effective_profile_filter"]["source_page"] == "dashboards_main"
|
|
assert payload["effective_profile_filter"]["override_show_all"] is False
|
|
assert payload["effective_profile_filter"]["username"] == "john_doe"
|
|
assert payload["effective_profile_filter"]["match_logic"] == "owners_or_modified_by"
|
|
# [/DEF:test_get_dashboards_profile_filter_contract_owners_or_modified_by:Function]
|
|
|
|
|
|
# [DEF:test_get_dashboards_override_show_all_contract:Function]
|
|
# @TEST: GET /api/dashboards honors override_show_all and disables profile-default filter for current page.
|
|
# @PRE: Profile-default preference exists but override_show_all=true query is provided.
|
|
# @POST: Response remains unfiltered and effective_profile_filter.applied is false.
|
|
def test_get_dashboards_override_show_all_contract(mock_deps):
|
|
mock_env = MagicMock()
|
|
mock_env.id = "prod"
|
|
mock_deps["config"].get_environments.return_value = [mock_env]
|
|
mock_deps["task"].get_all_tasks.return_value = []
|
|
mock_deps["resource"].get_dashboards_with_status = AsyncMock(return_value=[
|
|
{"id": 1, "title": "Dash A", "slug": "dash-a", "owners": ["john_doe"], "modified_by": "john_doe"},
|
|
{"id": 2, "title": "Dash B", "slug": "dash-b", "owners": ["other"], "modified_by": "other"},
|
|
])
|
|
|
|
with patch("src.api.routes.dashboards.ProfileService") as profile_service_cls:
|
|
profile_service = MagicMock()
|
|
profile_service.get_my_preference.return_value = _build_profile_preference_stub(
|
|
username="john_doe",
|
|
enabled=True,
|
|
)
|
|
profile_service.matches_dashboard_actor.side_effect = _matches_actor_case_insensitive
|
|
profile_service_cls.return_value = profile_service
|
|
|
|
response = client.get(
|
|
"/api/dashboards?env_id=prod&page_context=dashboards_main&apply_profile_default=true&override_show_all=true"
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
payload = response.json()
|
|
|
|
assert payload["total"] == 2
|
|
assert {item["id"] for item in payload["dashboards"]} == {1, 2}
|
|
assert payload["effective_profile_filter"]["applied"] is False
|
|
assert payload["effective_profile_filter"]["source_page"] == "dashboards_main"
|
|
assert payload["effective_profile_filter"]["override_show_all"] is True
|
|
assert payload["effective_profile_filter"]["username"] is None
|
|
assert payload["effective_profile_filter"]["match_logic"] is None
|
|
profile_service.matches_dashboard_actor.assert_not_called()
|
|
# [/DEF:test_get_dashboards_override_show_all_contract:Function]
|
|
|
|
|
|
# [DEF:test_get_dashboards_profile_filter_no_match_results_contract:Function]
|
|
# @TEST: GET /api/dashboards returns empty result set when profile-default filter is active and no dashboard actors match.
|
|
# @PRE: Profile-default preference is enabled with bound username and all dashboards are non-matching.
|
|
# @POST: Response total is 0 with deterministic pagination and active effective_profile_filter metadata.
|
|
def test_get_dashboards_profile_filter_no_match_results_contract(mock_deps):
|
|
mock_env = MagicMock()
|
|
mock_env.id = "prod"
|
|
mock_deps["config"].get_environments.return_value = [mock_env]
|
|
mock_deps["task"].get_all_tasks.return_value = []
|
|
mock_deps["resource"].get_dashboards_with_status = AsyncMock(return_value=[
|
|
{
|
|
"id": 101,
|
|
"title": "Team Dashboard",
|
|
"slug": "team-dashboard",
|
|
"owners": ["analytics-team"],
|
|
"modified_by": "someone_else",
|
|
},
|
|
{
|
|
"id": 102,
|
|
"title": "Ops Dashboard",
|
|
"slug": "ops-dashboard",
|
|
"owners": ["ops-user"],
|
|
"modified_by": "ops-user",
|
|
},
|
|
])
|
|
|
|
with patch("src.api.routes.dashboards.ProfileService") as profile_service_cls:
|
|
profile_service = MagicMock()
|
|
profile_service.get_my_preference.return_value = _build_profile_preference_stub(
|
|
username="john_doe",
|
|
enabled=True,
|
|
)
|
|
profile_service.matches_dashboard_actor.side_effect = _matches_actor_case_insensitive
|
|
profile_service_cls.return_value = profile_service
|
|
|
|
response = client.get(
|
|
"/api/dashboards?env_id=prod&page_context=dashboards_main&apply_profile_default=true"
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
payload = response.json()
|
|
|
|
assert payload["total"] == 0
|
|
assert payload["dashboards"] == []
|
|
assert payload["page"] == 1
|
|
assert payload["page_size"] == 10
|
|
assert payload["total_pages"] == 1
|
|
assert payload["effective_profile_filter"]["applied"] is True
|
|
assert payload["effective_profile_filter"]["source_page"] == "dashboards_main"
|
|
assert payload["effective_profile_filter"]["override_show_all"] is False
|
|
assert payload["effective_profile_filter"]["username"] == "john_doe"
|
|
assert payload["effective_profile_filter"]["match_logic"] == "owners_or_modified_by"
|
|
# [/DEF:test_get_dashboards_profile_filter_no_match_results_contract:Function]
|
|
|
|
|
|
# [DEF:test_get_dashboards_page_context_other_disables_profile_default:Function]
|
|
# @TEST: GET /api/dashboards does not auto-apply profile-default filter outside dashboards_main page context.
|
|
# @PRE: Profile-default preference exists but page_context=other query is provided.
|
|
# @POST: Response remains unfiltered and metadata reflects source_page=other.
|
|
def test_get_dashboards_page_context_other_disables_profile_default(mock_deps):
|
|
mock_env = MagicMock()
|
|
mock_env.id = "prod"
|
|
mock_deps["config"].get_environments.return_value = [mock_env]
|
|
mock_deps["task"].get_all_tasks.return_value = []
|
|
mock_deps["resource"].get_dashboards_with_status = AsyncMock(return_value=[
|
|
{"id": 1, "title": "Dash A", "slug": "dash-a", "owners": ["john_doe"], "modified_by": "john_doe"},
|
|
{"id": 2, "title": "Dash B", "slug": "dash-b", "owners": ["other"], "modified_by": "other"},
|
|
])
|
|
|
|
with patch("src.api.routes.dashboards.ProfileService") as profile_service_cls:
|
|
profile_service = MagicMock()
|
|
profile_service.get_my_preference.return_value = _build_profile_preference_stub(
|
|
username="john_doe",
|
|
enabled=True,
|
|
)
|
|
profile_service.matches_dashboard_actor.side_effect = _matches_actor_case_insensitive
|
|
profile_service_cls.return_value = profile_service
|
|
|
|
response = client.get(
|
|
"/api/dashboards?env_id=prod&page_context=other&apply_profile_default=true"
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
payload = response.json()
|
|
|
|
assert payload["total"] == 2
|
|
assert {item["id"] for item in payload["dashboards"]} == {1, 2}
|
|
assert payload["effective_profile_filter"]["applied"] is False
|
|
assert payload["effective_profile_filter"]["source_page"] == "other"
|
|
assert payload["effective_profile_filter"]["override_show_all"] is False
|
|
assert payload["effective_profile_filter"]["username"] is None
|
|
assert payload["effective_profile_filter"]["match_logic"] is None
|
|
profile_service.matches_dashboard_actor.assert_not_called()
|
|
# [/DEF:test_get_dashboards_page_context_other_disables_profile_default:Function]
|
|
|
|
|
|
# [DEF:test_get_dashboards_profile_filter_matches_display_alias_without_detail_fanout:Function]
|
|
# @TEST: GET /api/dashboards resolves Superset display-name alias once and filters without per-dashboard detail calls.
|
|
# @PRE: Profile-default filter is active, bound username is `admin`, dashboard actors contain display labels.
|
|
# @POST: Route matches by alias (`Superset Admin`) and does not call `SupersetClient.get_dashboard` in list filter path.
|
|
def test_get_dashboards_profile_filter_matches_display_alias_without_detail_fanout(mock_deps):
|
|
mock_env = MagicMock()
|
|
mock_env.id = "prod"
|
|
mock_deps["config"].get_environments.return_value = [mock_env]
|
|
mock_deps["task"].get_all_tasks.return_value = []
|
|
mock_deps["resource"].get_dashboards_with_status = AsyncMock(return_value=[
|
|
{
|
|
"id": 5,
|
|
"title": "Alias Match",
|
|
"slug": "alias-match",
|
|
"owners": [],
|
|
"created_by": None,
|
|
"modified_by": "Superset Admin",
|
|
},
|
|
{
|
|
"id": 6,
|
|
"title": "Alias No Match",
|
|
"slug": "alias-no-match",
|
|
"owners": [],
|
|
"created_by": None,
|
|
"modified_by": "Other User",
|
|
},
|
|
])
|
|
|
|
with patch("src.api.routes.dashboards.ProfileService") as profile_service_cls, patch(
|
|
"src.api.routes.dashboards.SupersetClient"
|
|
) as superset_client_cls, patch(
|
|
"src.api.routes.dashboards.SupersetAccountLookupAdapter"
|
|
) as lookup_adapter_cls:
|
|
profile_service = MagicMock()
|
|
profile_service.get_my_preference.return_value = _build_profile_preference_stub(
|
|
username="admin",
|
|
enabled=True,
|
|
)
|
|
profile_service.matches_dashboard_actor.side_effect = _matches_actor_case_insensitive
|
|
profile_service_cls.return_value = profile_service
|
|
|
|
superset_client = MagicMock()
|
|
superset_client_cls.return_value = superset_client
|
|
|
|
lookup_adapter = MagicMock()
|
|
lookup_adapter.get_users_page.return_value = {
|
|
"items": [
|
|
{
|
|
"environment_id": "prod",
|
|
"username": "admin",
|
|
"display_name": "Superset Admin",
|
|
"email": "admin@example.com",
|
|
"is_active": True,
|
|
}
|
|
],
|
|
"total": 1,
|
|
}
|
|
lookup_adapter_cls.return_value = lookup_adapter
|
|
|
|
response = client.get(
|
|
"/api/dashboards?env_id=prod&page_context=dashboards_main&apply_profile_default=true"
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
payload = response.json()
|
|
assert payload["total"] == 1
|
|
assert {item["id"] for item in payload["dashboards"]} == {5}
|
|
assert payload["effective_profile_filter"]["applied"] is True
|
|
lookup_adapter.get_users_page.assert_called_once()
|
|
superset_client.get_dashboard.assert_not_called()
|
|
# [/DEF:test_get_dashboards_profile_filter_matches_display_alias_without_detail_fanout:Function]
|
|
|
|
|
|
# [DEF:test_get_dashboards_profile_filter_matches_owner_object_payload_contract:Function]
|
|
# @TEST: GET /api/dashboards profile-default filter matches Superset owner object payloads.
|
|
# @PRE: Profile-default preference is enabled and owners list contains dict payloads.
|
|
# @POST: Response keeps dashboards where owner object resolves to bound username alias.
|
|
def test_get_dashboards_profile_filter_matches_owner_object_payload_contract(mock_deps):
|
|
mock_env = MagicMock()
|
|
mock_env.id = "prod"
|
|
mock_deps["config"].get_environments.return_value = [mock_env]
|
|
mock_deps["task"].get_all_tasks.return_value = []
|
|
mock_deps["resource"].get_dashboards_with_status = AsyncMock(return_value=[
|
|
{
|
|
"id": 701,
|
|
"title": "Featured Charts",
|
|
"slug": "featured-charts",
|
|
"owners": [
|
|
{
|
|
"id": 11,
|
|
"first_name": "user",
|
|
"last_name": "1",
|
|
"username": None,
|
|
"email": "user_1@example.local",
|
|
}
|
|
],
|
|
"modified_by": "another_user",
|
|
},
|
|
{
|
|
"id": 702,
|
|
"title": "Other Dashboard",
|
|
"slug": "other-dashboard",
|
|
"owners": [
|
|
{
|
|
"id": 12,
|
|
"first_name": "other",
|
|
"last_name": "user",
|
|
"username": None,
|
|
"email": "other@example.local",
|
|
}
|
|
],
|
|
"modified_by": "other_user",
|
|
},
|
|
])
|
|
|
|
with patch("src.api.routes.dashboards.ProfileService") as profile_service_cls, patch(
|
|
"src.api.routes.dashboards._resolve_profile_actor_aliases",
|
|
return_value=["user_1"],
|
|
):
|
|
profile_service = DomainProfileService(db=MagicMock(), config_manager=MagicMock())
|
|
profile_service.get_my_preference = MagicMock(
|
|
return_value=_build_profile_preference_stub(
|
|
username="user_1",
|
|
enabled=True,
|
|
)
|
|
)
|
|
profile_service_cls.return_value = profile_service
|
|
|
|
response = client.get(
|
|
"/api/dashboards?env_id=prod&page_context=dashboards_main&apply_profile_default=true"
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
payload = response.json()
|
|
assert payload["total"] == 1
|
|
assert {item["id"] for item in payload["dashboards"]} == {701}
|
|
assert payload["dashboards"][0]["title"] == "Featured Charts"
|
|
# [/DEF:test_get_dashboards_profile_filter_matches_owner_object_payload_contract:Function]
|
|
|
|
|
|
# [/DEF:backend.src.api.routes.__tests__.test_dashboards:Module]
|