# [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 # Global mock user for get_current_user dependency overrides mock_user = MagicMock() 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() 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[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 } 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): return [ {"id": 1, "title": "Sales Report", "slug": "sales"}, {"id": 2, "title": "Marketing Dashboard", "slug": "marketing"} ] 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:backend.src.api.routes.__tests__.test_dashboards:Module]