# #region Test.Api.Tasks [C:3] [TYPE Module] [SEMANTICS test,tasks,api,routes,crud] # @BRIEF API contract tests for task routes — create, list, get, update, cancel, and provider binding. # @RELATION BINDS_TO -> [TasksApi] # @TEST_EDGE: missing_plugin_id -> 400 when plugin_id missing # @TEST_EDGE: invalid_method -> 405 for unsupported HTTP method # @TEST_EDGE: task_not_found -> 404 for nonexistent task # @TEST_EDGE: unauthorized_access -> 401 when no auth token 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 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.tasks import router from src.core.database import get_db from src.dependencies import get_config_manager, get_current_user, get_task_manager from src.schemas.auth import User, RoleSchema app = FastAPI() app.include_router(router, prefix="/api/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_db] = lambda: MagicMock() app.dependency_overrides[get_current_user] = lambda: mock_user app.dependency_overrides[get_config_manager] = 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) # ── create_task ── class TestCreateTask: """POST /api/tasks""" def test_success(self): mock_tm = MagicMock() from src.core.task_manager import Task mock_task = MagicMock(spec=Task) mock_task.id = "task-1" mock_task.plugin_id = "backup" mock_task.status = "PENDING" mock_task.params = {} mock_task.created_at = None mock_task.user_id = "admin" mock_task.input_request = None mock_tm.create_task = AsyncMock(return_value=mock_task) from src.dependencies import get_task_manager client = _make_client({get_task_manager: lambda: mock_tm}) resp = client.post("/api/tasks", json={"plugin_id": "superset-backup", "params": {"dashboard_id": 42}}) assert resp.status_code == 201 def test_deprecated_llm_validation_returns_400(self): client = _make_client() resp = client.post("/api/tasks", json={ "plugin_id": "llm_dashboard_validation", "params": {"dashboard_ids": ["dash-1"], "environment_id": "env-1"}, }) assert resp.status_code == 400 def test_value_error(self): mock_tm = MagicMock() mock_tm.create_task = AsyncMock(side_effect=ValueError("Plugin not found")) from src.dependencies import get_task_manager client = _make_client({get_task_manager: lambda: mock_tm}) resp = client.post("/api/tasks", json={"plugin_id": "unknown", "params": {}}) assert resp.status_code == 404 # ── list_tasks ── class TestListTasks: """GET /api/tasks""" def test_success(self): mock_tm = MagicMock() mock_tm.get_tasks.return_value = [] from src.dependencies import get_task_manager client = _make_client({get_task_manager: lambda: mock_tm}) resp = client.get("/api/tasks") assert resp.status_code == 200 def test_with_filters(self): mock_tm = MagicMock() mock_tm.get_tasks.return_value = [] from src.dependencies import get_task_manager client = _make_client({get_task_manager: lambda: mock_tm}) resp = client.get("/api/tasks?limit=5&offset=10&status=RUNNING&completed_only=true") assert resp.status_code == 200 def test_with_plugin_ids(self): mock_tm = MagicMock() mock_tm.get_tasks.return_value = [] from src.dependencies import get_task_manager client = _make_client({get_task_manager: lambda: mock_tm}) resp = client.get("/api/tasks?plugin_id=backup&plugin_id=migration") assert resp.status_code == 200 def test_with_task_type(self): mock_tm = MagicMock() mock_tm.get_tasks.return_value = [] from src.dependencies import get_task_manager client = _make_client({get_task_manager: lambda: mock_tm}) resp = client.get("/api/tasks?task_type=llm_validation") assert resp.status_code == 200 def test_unsupported_task_type(self): client = _make_client() resp = client.get("/api/tasks?task_type=unsupported") assert resp.status_code == 400 def test_with_search(self): mock_tm = MagicMock() mock_tm.get_tasks.return_value = [] from src.dependencies import get_task_manager client = _make_client({get_task_manager: lambda: mock_tm}) resp = client.get("/api/tasks?search=backup") assert resp.status_code == 200 mock_tm.get_tasks.assert_called_once() assert mock_tm.get_tasks.call_args.kwargs["search"] == "backup" def test_with_search_max_length(self): mock_tm = MagicMock() mock_tm.get_tasks.return_value = [] from src.dependencies import get_task_manager client = _make_client({get_task_manager: lambda: mock_tm}) # max_length=200 on the Query param — this should pass resp = client.get(f"/api/tasks?search={'a' * 200}") assert resp.status_code == 200 def test_logs_cleared(self): from src.core.task_manager import Task mock_tm = MagicMock() mock_task = MagicMock(spec=Task) mock_task.id = "task-1" mock_task.logs = ["log_entry_1", "log_entry_2"] mock_tm.get_tasks.return_value = [mock_task] from src.dependencies import get_task_manager client = _make_client({get_task_manager: lambda: mock_tm}) resp = client.get("/api/tasks") assert resp.status_code == 200 assert mock_task.logs == [] # ── get_task ── class TestGetTask: """GET /api/tasks/{task_id}""" def test_success(self): mock_tm = MagicMock() from src.core.task_manager import Task mock_task = MagicMock(spec=Task) mock_task.id = "task-123" mock_task.plugin_id = "backup" mock_task.status = "PENDING" mock_task.params = {} mock_task.created_at = None mock_task.user_id = "admin" mock_task.input_request = None mock_tm.get_task.return_value = mock_task from src.dependencies import get_task_manager client = _make_client({get_task_manager: lambda: mock_tm}) resp = client.get("/api/tasks/task-123") assert resp.status_code == 200 assert resp.json()["id"] == "task-123" def test_not_found(self): mock_tm = MagicMock() mock_tm.get_task.return_value = None from src.dependencies import get_task_manager client = _make_client({get_task_manager: lambda: mock_tm}) resp = client.get("/api/tasks/unknown") assert resp.status_code == 404 # ── get_task_logs ── class TestGetTaskLogs: """GET /api/tasks/{task_id}/logs""" def test_success(self): mock_tm = MagicMock() mock_tm.get_task.return_value = MagicMock(id="task-123") mock_tm.get_task_logs.return_value = [MagicMock(level="INFO", message="test", timestamp=None, source="test")] from src.dependencies import get_task_manager client = _make_client({get_task_manager: lambda: mock_tm}) resp = client.get("/api/tasks/task-123/logs") assert resp.status_code == 200 def test_with_filters(self): mock_tm = MagicMock() mock_tm.get_task.return_value = MagicMock(id="task-123") from src.dependencies import get_task_manager client = _make_client({get_task_manager: lambda: mock_tm}) resp = client.get("/api/tasks/task-123/logs?level=INFO&source=worker&search=error&offset=0&limit=50") assert resp.status_code == 200 def test_task_not_found(self): mock_tm = MagicMock() mock_tm.get_task.return_value = None from src.dependencies import get_task_manager client = _make_client({get_task_manager: lambda: mock_tm}) resp = client.get("/api/tasks/unknown/logs") assert resp.status_code == 404 # ── get_task_log_stats ── class TestGetTaskLogStats: """GET /api/tasks/{task_id}/logs/stats""" def test_success_with_logstats(self): from src.core.task_manager.models import LogStats mock_tm = MagicMock() mock_tm.get_task.return_value = MagicMock(id="task-123") mock_tm.get_task_log_stats.return_value = LogStats(total_count=5, by_level={"INFO": 3}, by_source={"worker": 5}) from src.dependencies import get_task_manager client = _make_client({get_task_manager: lambda: mock_tm}) resp = client.get("/api/tasks/task-123/logs/stats") assert resp.status_code == 200 assert resp.json()["total_count"] == 5 def test_success_with_dict_payload(self): mock_tm = MagicMock() mock_tm.get_task.return_value = MagicMock(id="task-123") mock_tm.get_task_log_stats.return_value = {"total_count": 3, "by_level": {"INFO": 2}, "by_source": {"worker": 3}} from src.dependencies import get_task_manager client = _make_client({get_task_manager: lambda: mock_tm}) resp = client.get("/api/tasks/task-123/logs/stats") assert resp.status_code == 200 def test_success_with_flat_dict(self): mock_tm = MagicMock() mock_tm.get_task.return_value = MagicMock(id="task-123") mock_tm.get_task_log_stats.return_value = {"INFO": 3, "ERROR": 1} from src.dependencies import get_task_manager client = _make_client({get_task_manager: lambda: mock_tm}) resp = client.get("/api/tasks/task-123/logs/stats") assert resp.status_code == 200 assert resp.json()["total_count"] == 4 def test_task_not_found(self): mock_tm = MagicMock() mock_tm.get_task.return_value = None from src.dependencies import get_task_manager client = _make_client({get_task_manager: lambda: mock_tm}) resp = client.get("/api/tasks/unknown/logs/stats") assert resp.status_code == 404 # ── get_task_log_sources ── class TestGetTaskLogSources: """GET /api/tasks/{task_id}/logs/sources""" def test_success(self): mock_tm = MagicMock() mock_tm.get_task.return_value = MagicMock(id="task-123") mock_tm.get_task_log_sources.return_value = ["worker", "api"] from src.dependencies import get_task_manager client = _make_client({get_task_manager: lambda: mock_tm}) resp = client.get("/api/tasks/task-123/logs/sources") assert resp.status_code == 200 assert resp.json() == ["worker", "api"] def test_task_not_found(self): mock_tm = MagicMock() mock_tm.get_task.return_value = None from src.dependencies import get_task_manager client = _make_client({get_task_manager: lambda: mock_tm}) resp = client.get("/api/tasks/unknown/logs/sources") assert resp.status_code == 404 # ── resolve_task ── class TestResolveTask: """POST /api/tasks/{task_id}/resolve""" def test_success(self): mock_tm = MagicMock() mock_tm.resolve_task = AsyncMock() from src.core.task_manager import Task mock_task = MagicMock(spec=Task) mock_task.id = "task-123" mock_tm.get_task.return_value = mock_task from src.dependencies import get_task_manager client = _make_client({get_task_manager: lambda: mock_tm}) resp = client.post("/api/tasks/task-123/resolve", json={"resolution_params": {"key": "value"}}) assert resp.status_code == 200 def test_value_error(self): mock_tm = MagicMock() mock_tm.resolve_task = AsyncMock(side_effect=ValueError("Cannot resolve")) from src.dependencies import get_task_manager client = _make_client({get_task_manager: lambda: mock_tm}) resp = client.post("/api/tasks/task-123/resolve", json={"resolution_params": {}}) assert resp.status_code == 400 # ── resume_task ── class TestResumeTask: """POST /api/tasks/{task_id}/resume""" def test_success(self): mock_tm = MagicMock() mock_tm.resume_task_with_password = MagicMock() from src.core.task_manager import Task mock_task = MagicMock(spec=Task) mock_task.id = "task-123" mock_tm.get_task.return_value = mock_task from src.dependencies import get_task_manager client = _make_client({get_task_manager: lambda: mock_tm}) resp = client.post("/api/tasks/task-123/resume", json={"passwords": {"db": "pwd"}}) assert resp.status_code == 200 def test_value_error(self): mock_tm = MagicMock() mock_tm.resume_task_with_password = MagicMock(side_effect=ValueError("Cannot resume")) from src.dependencies import get_task_manager client = _make_client({get_task_manager: lambda: mock_tm}) resp = client.post("/api/tasks/task-123/resume", json={"passwords": {}}) assert resp.status_code == 400 # ── clear_tasks ── class TestClearTasks: """DELETE /api/tasks""" def test_success(self): mock_tm = MagicMock() from src.dependencies import get_task_manager client = _make_client({get_task_manager: lambda: mock_tm}) resp = client.delete("/api/tasks") assert resp.status_code == 204 def test_with_status(self): mock_tm = MagicMock() from src.dependencies import get_task_manager client = _make_client({get_task_manager: lambda: mock_tm}) resp = client.delete("/api/tasks?status=SUCCESS") assert resp.status_code == 204 def test_create_llm_documentation_task_with_provider(self): """Create task with llm_documentation — covers LLM provider block lines 80-123.""" from src.core.task_manager import Task mock_tm = MagicMock() mock_task = MagicMock(spec=Task) mock_task.id = "task-llm" mock_task.plugin_id = "llm_documentation" mock_task.status = "PENDING" mock_task.params = {"provider_id": "prov-1", "dashboard_id": 42} mock_task.created_at = None mock_task.user_id = "admin" mock_task.input_request = None mock_tm.create_task = AsyncMock(return_value=mock_task) mock_db = MagicMock() mock_llm_service = MagicMock() mock_provider = MagicMock() mock_provider.is_multimodal = True mock_llm_service.get_provider.return_value = mock_provider from src.dependencies import get_task_manager with patch("src.core.database.SessionLocal", return_value=mock_db), \ patch("src.services.llm_provider.LLMProviderService", return_value=mock_llm_service): client = _make_client({get_task_manager: lambda: mock_tm}) resp = client.post("/api/tasks", json={ "plugin_id": "llm_documentation", "params": {"provider_id": "prov-1", "dashboard_id": 42}, }) assert resp.status_code == 201 mock_db.close.assert_called_once() def test_create_llm_documentation_task_provider_not_found(self): """llm_documentation with invalid provider raises ValueError (line 112-113).""" mock_tm = MagicMock() mock_tm.create_task = AsyncMock() mock_db = MagicMock() mock_llm_service = MagicMock() mock_llm_service.get_provider.return_value = None from src.dependencies import get_task_manager with patch("src.core.database.SessionLocal", return_value=mock_db), \ patch("src.services.llm_provider.LLMProviderService", return_value=mock_llm_service): client = _make_client({get_task_manager: lambda: mock_tm}) resp = client.post("/api/tasks", json={ "plugin_id": "llm_documentation", "params": {"provider_id": "bad-prov"}, }) # ValueError is raised, caught by the outer except -> 404 assert resp.status_code == 404 mock_db.close.assert_called_once() def test_create_llm_documentation_no_provider_id_resolved(self): """llm_documentation without provider_id — resolved from config binding (lines 87-108).""" from src.core.task_manager import Task mock_tm = MagicMock() mock_task = MagicMock(spec=Task) mock_task.id = "task-llm" mock_task.plugin_id = "llm_documentation" mock_task.status = "PENDING" mock_task.params = {"dashboard_id": 42} mock_task.created_at = None mock_task.user_id = "admin" mock_task.input_request = None mock_tm.create_task = AsyncMock(return_value=mock_task) mock_db = MagicMock() mock_llm_service = MagicMock() mock_provider = MagicMock() mock_provider.is_multimodal = True mock_llm_service.get_provider.return_value = mock_provider mock_llm_service.get_all_providers.return_value = [] mock_config = MagicMock() mock_config.get_config.return_value.settings.llm = {} from src.dependencies import get_task_manager, get_config_manager with patch("src.core.database.SessionLocal", return_value=mock_db), \ patch("src.services.llm_provider.LLMProviderService", return_value=mock_llm_service), \ patch("src.api.routes.tasks.normalize_llm_settings", return_value={}), \ patch("src.api.routes.tasks.resolve_bound_provider_id", return_value=None): client = _make_client({ get_task_manager: lambda: mock_tm, get_config_manager: lambda: mock_config, }) resp = client.post("/api/tasks", json={ "plugin_id": "llm_documentation", "params": {"dashboard_id": 42}, }) assert resp.status_code == 201 mock_db.close.assert_called_once() # ── Additional edge cases ── def test_create_llm_documentation_provider_from_binding(self): """provider_id resolved from binding (line 100).""" from src.core.task_manager import Task mock_tm = MagicMock() mock_task = MagicMock(spec=Task) mock_task.id = "task-llm" mock_task.plugin_id = "llm_documentation" mock_task.status = "PENDING" mock_task.params = {"dashboard_id": 42} mock_task.created_at = None mock_task.user_id = "admin" mock_task.input_request = None mock_tm.create_task = AsyncMock(return_value=mock_task) mock_db = MagicMock() mock_llm_service = MagicMock() mock_provider = MagicMock() mock_provider.is_multimodal = True mock_llm_service.get_provider.return_value = mock_provider mock_llm_service.get_all_providers.return_value = [] from src.dependencies import get_task_manager with patch("src.core.database.SessionLocal", return_value=mock_db), \ patch("src.services.llm_provider.LLMProviderService", return_value=mock_llm_service), \ patch("src.api.routes.tasks.resolve_bound_provider_id", return_value="prov-binding"): client = _make_client({get_task_manager: lambda: mock_tm}) resp = client.post("/api/tasks", json={ "plugin_id": "llm_documentation", "params": {"dashboard_id": 42}, }) assert resp.status_code == 201 def test_create_llm_documentation_provider_from_active(self): """provider_id resolved from active provider fallback (lines 107-108).""" from src.core.task_manager import Task mock_tm = MagicMock() mock_task = MagicMock(spec=Task) mock_task.id = "task-llm" mock_task.plugin_id = "llm_documentation" mock_task.status = "PENDING" mock_task.params = {"dashboard_id": 42} mock_task.created_at = None mock_task.user_id = "admin" mock_task.input_request = None mock_tm.create_task = AsyncMock(return_value=mock_task) mock_db = MagicMock() mock_llm_service = MagicMock() mock_provider = MagicMock() mock_provider.id = "prov-active" mock_provider.is_active = True mock_provider.is_multimodal = True mock_llm_service.get_provider.return_value = mock_provider mock_llm_service.get_all_providers.return_value = [mock_provider] from src.dependencies import get_task_manager with patch("src.core.database.SessionLocal", return_value=mock_db), \ patch("src.services.llm_provider.LLMProviderService", return_value=mock_llm_service), \ patch("src.api.routes.tasks.resolve_bound_provider_id", return_value=None): client = _make_client({get_task_manager: lambda: mock_tm}) resp = client.post("/api/tasks", json={ "plugin_id": "llm_documentation", "params": {"dashboard_id": 42}, }) assert resp.status_code == 201 # #endregion Test.Api.Tasks