test: 6 agents — +52 test files across core, task_manager, translate routes, git/storage/migration routes, dataset_review deps/routes, settings. Fixed 4 failures
This commit is contained in:
472
backend/tests/api/test_validation_tasks_comprehensive.py
Normal file
472
backend/tests/api/test_validation_tasks_comprehensive.py
Normal file
@@ -0,0 +1,472 @@
|
||||
# #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 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)
|
||||
|
||||
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"}])
|
||||
|
||||
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):
|
||||
mock_svc = MagicMock()
|
||||
mock_svc.create_task.return_value = {"id": "task-new", "name": "New Task"}
|
||||
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.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"}
|
||||
|
||||
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):
|
||||
mock_svc = MagicMock()
|
||||
mock_svc.update_task.return_value = {"id": "task-1", "name": "Updated Task"}
|
||||
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.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.return_value = {"id": "task-1", "is_active": True}
|
||||
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.return_value = {"id": "task-1", "is_active": False}
|
||||
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.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()
|
||||
assert data["spawned_task_id"] == "task-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.return_value = (1, [{"id": "run-1"}])
|
||||
|
||||
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.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.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.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.api.routes.validation_tasks.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.api.routes.validation_tasks.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.api.routes.validation_tasks.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.api.routes.validation_tasks.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")
|
||||
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
|
||||
Reference in New Issue
Block a user