515 lines
20 KiB
Python
515 lines
20 KiB
Python
# ... (same header as before)
|
|
|
|
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
|
|
|
|
|
|
# ── 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
|