# #region Test.Api.ValidationTasks.Comprehensive [C:3] [TYPE Module] [SEMANTICS test,validation,tasks,comprehensive] # @BRIEF Comprehensive tests for validation task API — CRUD, runs, parse-url, redirect. # @RELATION BINDS_TO -> [ValidationTasksRoutes] 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") os.environ.setdefault("DEV_MODE", "true") import sys from datetime import datetime from pathlib import Path from unittest.mock import AsyncMock, MagicMock, patch import pytest from fastapi import FastAPI 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_client(overrides: dict | None = None) -> TestClient: from src.api.routes.validation_tasks import router, _get_task_service from src.core.database import get_db from src.dependencies import get_config_manager, get_current_user, get_scheduler_service, get_task_manager from src.schemas.auth import User, RoleSchema app = FastAPI() app.include_router(router, prefix="/validation-tasks") mock_user = User( id="admin-1", username="admin", email="admin@x.com", auth_source="LOCAL", created_at=__import__("datetime").datetime.now(), roles=[RoleSchema(id="r1", name="Admin", description="", permissions=[])], ) app.dependency_overrides[_get_task_service] = lambda: MagicMock() app.dependency_overrides[get_db] = lambda: MagicMock() app.dependency_overrides[get_current_user] = lambda: mock_user app.dependency_overrides[get_config_manager] = lambda: MagicMock() app.dependency_overrides[get_scheduler_service] = lambda: MagicMock() app.dependency_overrides[get_task_manager] = lambda: MagicMock() if overrides: for dep, fn in overrides.items(): app.dependency_overrides[dep] = fn return TestClient(app) # ── list_tasks ── class TestListTasks: """GET /validation-tasks""" def test_success(self): mock_svc = MagicMock() mock_svc.list_tasks.return_value = (1, [{ "id": "task-1", "name": "Test", "environment_id": "env-1", "is_active": True, "dashboard_ids": ["dash-1"], "created_at": "2024-01-01T00:00:00", }]) from src.api.routes.validation_tasks import _get_task_service client = _make_client({_get_task_service: lambda: mock_svc}) resp = client.get("/validation-tasks") assert resp.status_code == 200 data = resp.json() assert data["total"] == 1 assert len(data["items"]) == 1 def test_with_filters(self): mock_svc = MagicMock() mock_svc.list_tasks.return_value = (0, []) from src.api.routes.validation_tasks import _get_task_service client = _make_client({_get_task_service: lambda: mock_svc}) resp = client.get("/validation-tasks?is_active=true&environment_id=env-1&search=test") assert resp.status_code == 200 def test_value_error(self): mock_svc = MagicMock() mock_svc.list_tasks.side_effect = ValueError("Invalid filter") from src.api.routes.validation_tasks import _get_task_service client = _make_client({_get_task_service: lambda: mock_svc}) resp = client.get("/validation-tasks") assert resp.status_code == 400 # ── create_task ── class TestCreateTask: """POST /validation-tasks""" CREATE_PAYLOAD = { "name": "New Task", "environment_id": "env-1", "dashboard_ids": ["dash-1"], "provider_id": "provider-1", } def test_success(self): from src.schemas.validation import ValidationTaskResponse mock_svc = MagicMock() mock_svc.create_task = AsyncMock(return_value=ValidationTaskResponse( id="task-new", name="New Task", environment_id="env-1", is_active=True, dashboard_ids=["dash-1"], created_at=datetime.now(), )) mock_sched = MagicMock() from src.api.routes.validation_tasks import _get_task_service from src.dependencies import get_scheduler_service client = _make_client({ _get_task_service: lambda: mock_svc, get_scheduler_service: lambda: mock_sched, }) resp = client.post("/validation-tasks", json=self.CREATE_PAYLOAD) assert resp.status_code == 201 mock_sched.reload_validation_policy.assert_called_once() def test_value_error(self): mock_svc = MagicMock() mock_svc.create_task = AsyncMock(side_effect=ValueError("Invalid provider")) from src.api.routes.validation_tasks import _get_task_service client = _make_client({_get_task_service: lambda: mock_svc}) resp = client.post("/validation-tasks", json=self.CREATE_PAYLOAD) assert resp.status_code == 422 # ── get_task ── class TestGetTask: """GET /validation-tasks/{policy_id}""" def test_success(self): mock_svc = MagicMock() mock_svc.get_task.return_value = { "id": "task-1", "name": "Test Task", "environment_id": "env-1", "is_active": True, "dashboard_ids": ["dash-1"], "created_at": "2024-01-01T00:00:00", } from src.api.routes.validation_tasks import _get_task_service client = _make_client({_get_task_service: lambda: mock_svc}) resp = client.get("/validation-tasks/task-1") assert resp.status_code == 200 assert resp.json()["id"] == "task-1" def test_not_found(self): mock_svc = MagicMock() mock_svc.get_task.side_effect = ValueError("Task not found") from src.api.routes.validation_tasks import _get_task_service client = _make_client({_get_task_service: lambda: mock_svc}) resp = client.get("/validation-tasks/unknown") assert resp.status_code == 404 # ── update_task ── class TestUpdateTask: """PUT /validation-tasks/{policy_id}""" UPDATE_PAYLOAD = {"name": "Updated Task"} def test_success(self): from src.schemas.validation import ValidationTaskResponse mock_svc = MagicMock() mock_svc.update_task = AsyncMock(return_value=ValidationTaskResponse( id="task-1", name="Updated Task", environment_id="env-1", is_active=True, dashboard_ids=["dash-1"], created_at=datetime.now(), )) mock_sched = MagicMock() from src.api.routes.validation_tasks import _get_task_service from src.dependencies import get_scheduler_service client = _make_client({ _get_task_service: lambda: mock_svc, get_scheduler_service: lambda: mock_sched, }) resp = client.put("/validation-tasks/task-1", json=self.UPDATE_PAYLOAD) assert resp.status_code == 200 mock_sched.reload_validation_policy.assert_called_once() def test_not_found(self): mock_svc = MagicMock() mock_svc.update_task = AsyncMock(side_effect=ValueError("Task not found")) from src.api.routes.validation_tasks import _get_task_service client = _make_client({_get_task_service: lambda: mock_svc}) resp = client.put("/validation-tasks/unknown", json=self.UPDATE_PAYLOAD) assert resp.status_code == 404 # ── toggle_task_status ── class TestToggleTaskStatus: """PATCH /validation-tasks/{policy_id}/status""" def test_activate(self): mock_svc = MagicMock() mock_svc.update_task = AsyncMock(return_value={ "id": "task-1", "name": "Task", "environment_id": "env-1", "is_active": True, "dashboard_ids": ["dash-1"], "created_at": "2024-01-01T00:00:00", }) mock_sched = MagicMock() from src.api.routes.validation_tasks import _get_task_service from src.dependencies import get_scheduler_service client = _make_client({ _get_task_service: lambda: mock_svc, get_scheduler_service: lambda: mock_sched, }) resp = client.patch("/validation-tasks/task-1/status?is_active=true") assert resp.status_code == 200 mock_sched.reload_validation_policy.assert_called_once() def test_deactivate(self): mock_svc = MagicMock() mock_svc.update_task = AsyncMock(return_value={ "id": "task-1", "name": "Task", "environment_id": "env-1", "is_active": False, "dashboard_ids": ["dash-1"], "created_at": "2024-01-01T00:00:00", }) mock_sched = MagicMock() from src.api.routes.validation_tasks import _get_task_service from src.dependencies import get_scheduler_service client = _make_client({ _get_task_service: lambda: mock_svc, get_scheduler_service: lambda: mock_sched, }) resp = client.patch("/validation-tasks/task-1/status?is_active=false") assert resp.status_code == 200 mock_sched.remove_validation_job.assert_called_once() def test_not_found(self): mock_svc = MagicMock() mock_svc.update_task = AsyncMock(side_effect=ValueError("Task not found")) from src.api.routes.validation_tasks import _get_task_service client = _make_client({_get_task_service: lambda: mock_svc}) resp = client.patch("/validation-tasks/unknown/status?is_active=true") assert resp.status_code == 404 # ── trigger_run ── class TestTriggerRun: """POST /validation-tasks/{policy_id}/run""" def test_success(self): mock_svc = MagicMock() mock_svc.trigger_run = AsyncMock(return_value={"task_id": "t-1", "run_id": "run-1"}) from src.api.routes.validation_tasks import _get_task_service client = _make_client({_get_task_service: lambda: mock_svc}) resp = client.post("/validation-tasks/task-1/run") assert resp.status_code == 200 data = resp.json() # spawned_task_id comes from result["task_id"] (the mock returns "t-1") assert data["spawned_task_id"] == "t-1" assert data["status"] == "PENDING" def test_value_error(self): mock_svc = MagicMock() mock_svc.trigger_run = AsyncMock(side_effect=ValueError("Cannot trigger")) from src.api.routes.validation_tasks import _get_task_service client = _make_client({_get_task_service: lambda: mock_svc}) resp = client.post("/validation-tasks/task-1/run") assert resp.status_code == 422 # ── list_all_runs ── class TestListAllRuns: """GET /validation-tasks/runs/all""" def test_success(self): mock_svc = MagicMock() mock_svc.list_all_runs = AsyncMock(return_value=(1, [{ "id": "run-1", "dashboard_id": "dash-1", "status": "SUCCESS", }])) from src.api.routes.validation_tasks import _get_task_service client = _make_client({_get_task_service: lambda: mock_svc}) resp = client.get("/validation-tasks/runs/all") assert resp.status_code == 200 data = resp.json() assert data["total"] == 1 def test_with_filters(self): mock_svc = MagicMock() mock_svc.list_all_runs = AsyncMock(return_value=(0, [])) from src.api.routes.validation_tasks import _get_task_service client = _make_client({_get_task_service: lambda: mock_svc}) resp = client.get("/validation-tasks/runs/all?status=RUNNING&environment_id=env-1&dashboard_id=dash-1") assert resp.status_code == 200 def test_value_error(self): mock_svc = MagicMock() mock_svc.list_all_runs = AsyncMock(side_effect=ValueError("Invalid filter")) from src.api.routes.validation_tasks import _get_task_service client = _make_client({_get_task_service: lambda: mock_svc}) resp = client.get("/validation-tasks/runs/all") assert resp.status_code == 400 # ── list_runs ── class TestListRuns: """GET /validation-tasks/{policy_id}/runs""" def test_success(self): mock_svc = MagicMock() mock_svc.get_run_history.return_value = (1, [{ "id": "run-1", "policy_id": "task-1", "status": "SUCCESS", "trigger": "manual", "dashboard_count": 2, "pass_count": 1, "warn_count": 0, "fail_count": 0, "unknown_count": 0, }]) from src.api.routes.validation_tasks import _get_task_service client = _make_client({_get_task_service: lambda: mock_svc}) resp = client.get("/validation-tasks/task-1/runs") assert resp.status_code == 200 data = resp.json() assert data["total"] == 1 assert data["items"][0]["status"] == "SUCCESS" def test_value_error(self): mock_svc = MagicMock() mock_svc.get_run_history.side_effect = ValueError("Task not found") from src.api.routes.validation_tasks import _get_task_service client = _make_client({_get_task_service: lambda: mock_svc}) resp = client.get("/validation-tasks/unknown/runs") assert resp.status_code == 400 # ── get_run_detail ── class TestGetRunDetail: """GET /validation-tasks/{policy_id}/runs/{run_id}""" def test_success(self): mock_svc = MagicMock() mock_svc.get_run_detail = AsyncMock(return_value={ "run": {"id": "run-1", "policy_id": "task-1", "status": "SUCCESS", "trigger": "manual", "dashboard_count": 2, "pass_count": 1, "warn_count": 0, "fail_count": 0, "unknown_count": 0}, "records": [{"dashboard_id": "dash-1", "status": "PASS"}], }) from src.api.routes.validation_tasks import _get_task_service client = _make_client({_get_task_service: lambda: mock_svc}) resp = client.get("/validation-tasks/task-1/runs/run-1") assert resp.status_code == 200 data = resp.json() assert data["run"]["status"] == "SUCCESS" assert len(data["records"]) == 1 def test_not_found(self): mock_svc = MagicMock() mock_svc.get_run_detail.side_effect = ValueError("Run not found") from src.api.routes.validation_tasks import _get_task_service client = _make_client({_get_task_service: lambda: mock_svc}) resp = client.get("/validation-tasks/task-1/runs/unknown") assert resp.status_code == 404 # ── parse_superset_url ── class TestParseSupersetUrl: """POST /validation-tasks/parse-url""" PARSE_PAYLOAD = { "url": "https://superset.example.com/superset/dashboard/1/", "environment_id": "env-1", } def test_success_with_dashboard_id(self): mock_config = MagicMock() mock_config.get_environment.return_value = MagicMock() mock_extractor = MagicMock() mock_parsed = MagicMock() mock_parsed.dashboard_id = 1 mock_parsed.chart_id = None mock_parsed.query_state = {} mock_parsed.partial_recovery = None mock_parsed.unresolved_references = None mock_extractor.parse_superset_link = AsyncMock(return_value=mock_parsed) mock_client = MagicMock() mock_client.get_dashboard_detail = AsyncMock(return_value={"dashboard_title": "My Dashboard"}) mock_extractor.client = mock_client from src.dependencies import get_config_manager with patch("src.core.utils.superset_context_extractor.SupersetContextExtractor", return_value=mock_extractor): client = _make_client({get_config_manager: lambda: mock_config}) resp = client.post("/validation-tasks/parse-url", json=self.PARSE_PAYLOAD) assert resp.status_code == 200 data = resp.json() assert data["dashboard_id"] == "1" assert data["title"] == "My Dashboard" def test_success_with_chart_id(self): mock_config = MagicMock() mock_config.get_environment.return_value = MagicMock() mock_extractor = MagicMock() mock_parsed = MagicMock() mock_parsed.dashboard_id = None mock_parsed.chart_id = 42 mock_parsed.query_state = {} mock_parsed.partial_recovery = None mock_parsed.unresolved_references = None mock_extractor.parse_superset_link = AsyncMock(return_value=mock_parsed) mock_extractor.client = MagicMock() from src.dependencies import get_config_manager with patch("src.core.utils.superset_context_extractor.SupersetContextExtractor", return_value=mock_extractor): client = _make_client({get_config_manager: lambda: mock_config}) resp = client.post("/validation-tasks/parse-url", json=self.PARSE_PAYLOAD) assert resp.status_code == 200 assert resp.json()["title"] == "chart:42" def test_environment_not_found(self): mock_config = MagicMock() mock_config.get_environment.return_value = None from src.dependencies import get_config_manager client = _make_client({get_config_manager: lambda: mock_config}) resp = client.post("/validation-tasks/parse-url", json=self.PARSE_PAYLOAD) assert resp.status_code == 404 def test_value_error(self): mock_config = MagicMock() mock_config.get_environment.return_value = MagicMock() mock_extractor = MagicMock() mock_extractor.parse_superset_link = AsyncMock(side_effect=ValueError("Invalid URL")) from src.dependencies import get_config_manager with patch("src.core.utils.superset_context_extractor.SupersetContextExtractor", return_value=mock_extractor): client = _make_client({get_config_manager: lambda: mock_config}) resp = client.post("/validation-tasks/parse-url", json=self.PARSE_PAYLOAD) assert resp.status_code == 422 def test_unexpected_error(self): mock_config = MagicMock() mock_config.get_environment.return_value = MagicMock() mock_extractor = MagicMock() mock_extractor.parse_superset_link = AsyncMock(side_effect=RuntimeError("Network error")) from src.dependencies import get_config_manager with patch("src.core.utils.superset_context_extractor.SupersetContextExtractor", return_value=mock_extractor): client = _make_client({get_config_manager: lambda: mock_config}) resp = client.post("/validation-tasks/parse-url", json=self.PARSE_PAYLOAD) assert resp.status_code == 502 # ── legacy_llm_report_redirect ── class TestLegacyLlmReportRedirect: """GET /validation-tasks/reports/llm/{task_id}""" def test_found(self): mock_db = MagicMock() mock_run = MagicMock() mock_run.task_id = "old-task" mock_run.policy_id = "policy-1" mock_run.id = "run-1" mock_db.query.return_value.filter.return_value.first.return_value = mock_run from src.core.database import get_db client = _make_client({get_db: lambda: mock_db}) resp = client.get("/validation-tasks/reports/llm/old-task", follow_redirects=False) assert resp.status_code == 302 assert "/validation-tasks/policy-1/runs/run-1" in resp.headers["location"] def test_not_found(self): mock_db = MagicMock() mock_db.query.return_value.filter.return_value.first.return_value = None from src.core.database import get_db client = _make_client({get_db: lambda: mock_db}) resp = client.get("/validation-tasks/reports/llm/unknown") assert resp.status_code == 404 # #endregion Test.Api.ValidationTasks.Comprehensive