# [DEF:TestResourceHubs:Module] # @RELATION: DEPENDS_ON -> [DashboardsApi] # @RELATION: DEPENDS_ON -> [DatasetsApi] # @COMPLEXITY: 3 # @SEMANTICS: tests, resource-hubs, dashboards, datasets, pagination, api # @PURPOSE: Contract tests for resource hub dashboards/datasets listing and pagination boundary validation. # @LAYER: Domain (Tests) import pytest from fastapi.testclient import TestClient from unittest.mock import MagicMock, AsyncMock from src.app import app from src.dependencies import ( get_config_manager, get_task_manager, get_resource_service, has_permission, ) client = TestClient(app) # [DEF:test_dashboards_api:Block] # @RELATION: BINDS_TO -> [TestResourceHubs] # @PURPOSE: Verify GET /api/dashboards contract compliance # @TEST_CONTRACT: dashboards_query -> dashboards payload or not_found response # @TEST_SCENARIO: dashboards_env_found_returns_payload -> HTTP 200 returns normalized dashboards list. # @TEST_SCENARIO: dashboards_unknown_env_returns_not_found -> HTTP 404 is returned for unknown env_id. # @TEST_SCENARIO: dashboards_search_filters_results -> Search narrows payload to matching dashboard title. # @TEST_INVARIANT: dashboards_route_contract_stays_observable -> VERIFIED_BY: [dashboards_env_found_returns_payload, dashboards_unknown_env_returns_not_found, dashboards_search_filters_results] # [DEF:mock_deps:Function] # @PURPOSE: Provide dependency override fixture for resource hub route tests. # @RELATION: BINDS_TO -> [TestResourceHubs] # @TEST_FIXTURE: resource_hub_overrides -> INLINE_JSON @pytest.fixture def mock_deps(): # @INVARIANT: unconstrained mock — no spec= enforced; attribute typos will silently pass config_manager = MagicMock() # @INVARIANT: unconstrained mock — no spec= enforced; attribute typos will silently pass task_manager = MagicMock() # @INVARIANT: unconstrained mock — no spec= enforced; attribute typos will silently pass resource_service = MagicMock() # Mock environment env = MagicMock() env.id = "env1" config_manager.get_environments.return_value = [env] # Mock tasks task_manager.get_all_tasks.return_value = [] # Mock dashboards resource_service.get_dashboards_with_status = AsyncMock( return_value=[ { "id": 1, "title": "Sales", "slug": "sales", "git_status": {"branch": "main", "sync_status": "OK"}, "last_task": None, }, { "id": 2, "title": "Marketing", "slug": "mkt", "git_status": None, "last_task": {"task_id": "t1", "status": "SUCCESS"}, }, ] ) 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 # Bypass permission check mock_user = MagicMock() mock_user.username = "testadmin" mock_user.roles = [] admin_role = MagicMock() admin_role.name = "Admin" mock_user.roles.append(admin_role) # Override both get_current_user and has_permission from src.dependencies import get_current_user app.dependency_overrides[get_current_user] = lambda: mock_user # We need to override the specific instance returned by has_permission app.dependency_overrides[has_permission("plugin:migration", "READ")] = ( lambda: mock_user ) yield {"config": config_manager, "task": task_manager, "resource": resource_service} app.dependency_overrides.clear() # [/DEF:mock_deps:Function] # [DEF:test_get_dashboards_success:Function] # @RELATION: BINDS_TO -> [test_dashboards_api] # @PURPOSE: Verify dashboards endpoint returns 200 with expected dashboard payload fields. def test_get_dashboards_success(mock_deps): response = client.get("/api/dashboards?env_id=env1") assert response.status_code == 200 data = response.json() assert "dashboards" in data assert len(data["dashboards"]) == 2 assert data["dashboards"][0]["title"] == "Sales" assert data["dashboards"][0]["git_status"]["sync_status"] == "OK" # [/DEF:test_get_dashboards_success:Function] # [DEF:test_get_dashboards_not_found:Function] # @RELATION: BINDS_TO -> [test_dashboards_api] # @PURPOSE: Verify dashboards endpoint returns 404 for unknown environment identifier. def test_get_dashboards_not_found(mock_deps): response = client.get("/api/dashboards?env_id=invalid") assert response.status_code == 404 # [/DEF:test_get_dashboards_not_found:Function] # [DEF:test_get_dashboards_search:Function] # @RELATION: BINDS_TO -> [test_dashboards_api] # @PURPOSE: Verify dashboards endpoint search filter returns matching subset. def test_get_dashboards_search(mock_deps): response = client.get("/api/dashboards?env_id=env1&search=Sales") assert response.status_code == 200 data = response.json() assert len(data["dashboards"]) == 1 assert data["dashboards"][0]["title"] == "Sales" # [/DEF:test_get_dashboards_search:Function] # [/DEF:test_dashboards_api:Block] # [DEF:test_datasets_api:Block] # @RELATION: BINDS_TO -> [TestResourceHubs] # @PURPOSE: Verify GET /api/datasets contract compliance # @TEST_CONTRACT: datasets_query -> datasets payload or error response # @TEST_SCENARIO: datasets_env_found_returns_payload -> HTTP 200 returns normalized datasets list. # @TEST_SCENARIO: datasets_unknown_env_returns_not_found -> HTTP 404 is returned for unknown env_id. # @TEST_SCENARIO: datasets_search_filters_results -> Search narrows payload to matching dataset table. # @TEST_SCENARIO: datasets_service_failure_returns_503 -> Backend fetch failure surfaces as HTTP 503. # @TEST_INVARIANT: datasets_route_contract_stays_observable -> VERIFIED_BY: [datasets_env_found_returns_payload, datasets_unknown_env_returns_not_found, datasets_search_filters_results, datasets_service_failure_returns_503] # [DEF:test_get_datasets_success:Function] # @RELATION: BINDS_TO -> [test_datasets_api] # @PURPOSE: Verify datasets endpoint returns 200 with mapped fields payload. def test_get_datasets_success(mock_deps): mock_deps["resource"].get_datasets_with_status = AsyncMock( return_value=[ { "id": 1, "table_name": "orders", "schema": "public", "database": "db1", "mapped_fields": {"total": 10, "mapped": 5}, "last_task": None, } ] ) response = client.get("/api/datasets?env_id=env1") assert response.status_code == 200 data = response.json() assert "datasets" in data assert len(data["datasets"]) == 1 assert data["datasets"][0]["table_name"] == "orders" assert data["datasets"][0]["mapped_fields"]["mapped"] == 5 # [/DEF:test_get_datasets_success:Function] # [DEF:test_get_datasets_not_found:Function] # @RELATION: BINDS_TO -> [test_datasets_api] # @PURPOSE: Verify datasets endpoint returns 404 for unknown environment identifier. def test_get_datasets_not_found(mock_deps): response = client.get("/api/datasets?env_id=invalid") assert response.status_code == 404 # [/DEF:test_get_datasets_not_found:Function] # [DEF:test_get_datasets_search:Function] # @RELATION: BINDS_TO -> [test_datasets_api] # @PURPOSE: Verify datasets endpoint search filter returns matching dataset subset. def test_get_datasets_search(mock_deps): mock_deps["resource"].get_datasets_with_status = AsyncMock( return_value=[ { "id": 1, "table_name": "orders", "schema": "public", "database": "db1", "mapped_fields": {"total": 10, "mapped": 5}, "last_task": None, }, { "id": 2, "table_name": "users", "schema": "public", "database": "db1", "mapped_fields": {"total": 5, "mapped": 5}, "last_task": None, }, ] ) response = client.get("/api/datasets?env_id=env1&search=orders") assert response.status_code == 200 data = response.json() assert len(data["datasets"]) == 1 assert data["datasets"][0]["table_name"] == "orders" # [/DEF:test_get_datasets_search:Function] # [DEF:test_get_datasets_service_failure:Function] # @RELATION: BINDS_TO -> [test_datasets_api] # @PURPOSE: Verify datasets endpoint surfaces backend fetch failure as HTTP 503. def test_get_datasets_service_failure(mock_deps): mock_deps["resource"].get_datasets_with_status = AsyncMock( side_effect=Exception("Superset down") ) response = client.get("/api/datasets?env_id=env1") assert response.status_code == 503 assert "Failed to fetch datasets" in response.json()["detail"] # [/DEF:test_get_datasets_service_failure:Function] # [/DEF:test_datasets_api:Block] # [DEF:test_pagination_boundaries:Block] # @RELATION: BINDS_TO -> [TestResourceHubs] # @PURPOSE: Verify pagination validation for GET endpoints # @TEST_CONTRACT: pagination_query -> validation error response # @TEST_SCENARIO: dashboards_zero_page_rejected -> page=0 returns HTTP 400. # @TEST_SCENARIO: dashboards_oversize_page_rejected -> page_size=101 returns HTTP 400. # @TEST_SCENARIO: datasets_zero_page_rejected -> page=0 returns HTTP 400. # @TEST_SCENARIO: datasets_oversize_page_rejected -> page_size=101 returns HTTP 400. # @TEST_EDGE: missing_field -> Missing env_id prevents route contract completion. # @TEST_EDGE: invalid_type -> Invalid pagination values are rejected at route validation layer. # @TEST_EDGE: external_fail -> Validation failure returns HTTP 400 without partial payload. # @TEST_INVARIANT: pagination_limits_apply_to_both_routes -> VERIFIED_BY: [dashboards_zero_page_rejected, dashboards_oversize_page_rejected, datasets_zero_page_rejected, datasets_oversize_page_rejected] # [DEF:test_get_dashboards_pagination_zero_page:Function] # @RELATION: BINDS_TO -> [test_pagination_boundaries] # @PURPOSE: Verify dashboards endpoint rejects page=0 with HTTP 400 validation error. def test_get_dashboards_pagination_zero_page(mock_deps): # @TEST_EDGE: pagination_zero_page -> {page: 0, status: 400} response = client.get("/api/dashboards?env_id=env1&page=0") assert response.status_code == 400 assert "Page must be >= 1" in response.json()["detail"] # [/DEF:test_get_dashboards_pagination_zero_page:Function] # [DEF:test_get_dashboards_pagination_oversize:Function] # @RELATION: BINDS_TO -> [test_pagination_boundaries] # @PURPOSE: Verify dashboards endpoint rejects oversized page_size with HTTP 400. def test_get_dashboards_pagination_oversize(mock_deps): # @TEST_EDGE: pagination_oversize -> {page_size: 101, status: 400} response = client.get("/api/dashboards?env_id=env1&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_pagination_oversize:Function] # [DEF:test_get_datasets_pagination_zero_page:Function] # @RELATION: BINDS_TO -> [test_pagination_boundaries] # @PURPOSE: Verify datasets endpoint rejects page=0 with HTTP 400. def test_get_datasets_pagination_zero_page(mock_deps): # @TEST_EDGE: pagination_zero_page_datasets -> {page: 0, status: 400} response = client.get("/api/datasets?env_id=env1&page=0") assert response.status_code == 400 # [/DEF:test_get_datasets_pagination_zero_page:Function] # [DEF:test_get_datasets_pagination_oversize:Function] # @RELATION: BINDS_TO -> [test_pagination_boundaries] # @PURPOSE: Verify datasets endpoint rejects oversized page_size with HTTP 400. def test_get_datasets_pagination_oversize(mock_deps): # @TEST_EDGE: pagination_oversize_datasets -> {page_size: 101, status: 400} response = client.get("/api/datasets?env_id=env1&page_size=101") assert response.status_code == 400 # [/DEF:test_get_datasets_pagination_oversize:Function] # [/DEF:test_pagination_boundaries:Block] # [/DEF:TestResourceHubs:Module]