Some checks failed
Tests / Backend Tests (Python) (3.10) (push) Has been cancelled
Tests / Backend Tests (Python) (3.11) (push) Has been cancelled
Tests / Backend Tests (Python) (3.12) (push) Has been cancelled
Tests / Frontend Tests (JS) (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / All Tests Passed (push) Has been cancelled
620 lines
20 KiB
Python
620 lines
20 KiB
Python
"""
|
|
Tests pour les routes de gestion des tâches.
|
|
|
|
Couvre:
|
|
- Liste des tâches
|
|
- Tâches en cours
|
|
- Logs de tâches
|
|
- Exécution de tâches
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import patch, AsyncMock, MagicMock
|
|
from httpx import AsyncClient
|
|
|
|
pytestmark = pytest.mark.unit
|
|
|
|
|
|
class TestGetTasks:
|
|
"""Tests pour GET /api/tasks."""
|
|
|
|
async def test_list_tasks_empty(self, client: AsyncClient, db_session):
|
|
"""Liste vide quand pas de tâches."""
|
|
response = await client.get("/api/tasks")
|
|
|
|
assert response.status_code == 200
|
|
assert isinstance(response.json(), list)
|
|
|
|
async def test_list_tasks_with_data(
|
|
self, client: AsyncClient, db_session, task_factory
|
|
):
|
|
"""Liste les tâches depuis la BD."""
|
|
await task_factory.create(db_session, action="health-check", target="all")
|
|
await task_factory.create(db_session, action="backup", target="server1")
|
|
|
|
response = await client.get("/api/tasks")
|
|
|
|
assert response.status_code == 200
|
|
tasks = response.json()
|
|
assert len(tasks) >= 2
|
|
|
|
async def test_list_tasks_with_pagination(
|
|
self, client: AsyncClient, db_session, task_factory
|
|
):
|
|
"""Pagination fonctionne."""
|
|
for i in range(5):
|
|
await task_factory.create(db_session, action=f"task-{i}", target="all")
|
|
|
|
response = await client.get("/api/tasks?limit=2&offset=0")
|
|
|
|
assert response.status_code == 200
|
|
tasks = response.json()
|
|
assert isinstance(tasks, list)
|
|
|
|
async def test_list_tasks_structure(
|
|
self, client: AsyncClient, db_session, task_factory
|
|
):
|
|
"""Vérifie la structure de réponse."""
|
|
await task_factory.create(
|
|
db_session,
|
|
action="test-action",
|
|
target="test-host",
|
|
status="completed"
|
|
)
|
|
|
|
response = await client.get("/api/tasks")
|
|
|
|
assert response.status_code == 200
|
|
tasks = response.json()
|
|
assert len(tasks) >= 1
|
|
|
|
task = tasks[0]
|
|
assert "id" in task
|
|
assert "name" in task
|
|
assert "host" in task
|
|
assert "status" in task
|
|
|
|
|
|
class TestGetRunningTasks:
|
|
"""Tests pour GET /api/tasks/running."""
|
|
|
|
async def test_running_tasks_empty(self, client: AsyncClient, db_session):
|
|
"""Aucune tâche en cours."""
|
|
response = await client.get("/api/tasks/running")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "tasks" in data
|
|
assert "count" in data
|
|
assert data["count"] == 0
|
|
|
|
async def test_running_tasks_with_running(
|
|
self, client: AsyncClient, db_session, task_factory
|
|
):
|
|
"""Tâches en cours retournées."""
|
|
await task_factory.create(db_session, action="running-task", status="running")
|
|
await task_factory.create(db_session, action="completed-task", status="completed")
|
|
|
|
response = await client.get("/api/tasks/running")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "tasks" in data
|
|
# Only running tasks should be returned
|
|
for task in data["tasks"]:
|
|
assert task["status"] in ("running", "pending")
|
|
|
|
|
|
class TestTaskLogs:
|
|
"""Tests pour GET /api/tasks/logs."""
|
|
|
|
async def test_get_task_logs(self, client: AsyncClient):
|
|
"""Récupère les logs de tâches."""
|
|
response = await client.get("/api/tasks/logs")
|
|
|
|
assert response.status_code == 200
|
|
|
|
async def test_get_task_logs_with_filters(self, client: AsyncClient):
|
|
"""Logs avec filtres."""
|
|
response = await client.get("/api/tasks/logs?status=success&limit=10")
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
class TestExecuteTask:
|
|
"""Tests pour POST /api/tasks."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_execute_task_endpoint_exists(self, client: AsyncClient):
|
|
"""Endpoint d'exécution existe."""
|
|
# Just verify the endpoint responds (auth is overridden in tests)
|
|
response = await client.post(
|
|
"/api/tasks",
|
|
json={"action": "health-check", "target": "all"}
|
|
)
|
|
|
|
# Should get a response (may be 200, 400, or 422 depending on validation)
|
|
assert response.status_code in [200, 400, 422]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_execute_task_missing_action(self, client: AsyncClient):
|
|
"""Erreur si action manquante."""
|
|
response = await client.post(
|
|
"/api/tasks",
|
|
json={"target": "all"}
|
|
)
|
|
|
|
assert response.status_code == 422
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_execute_task_with_host(self, client: AsyncClient):
|
|
"""Exécution sur un hôte spécifique."""
|
|
with patch("app.routes.tasks.ansible_service") as mock_ansible, \
|
|
patch("app.routes.tasks.ws_manager") as mock_ws, \
|
|
patch("app.routes.tasks.db") as mock_db:
|
|
mock_ansible.host_exists.return_value = True
|
|
mock_ansible.execute_playbook = AsyncMock(return_value=MagicMock(
|
|
return_code=0,
|
|
stdout="Success",
|
|
stderr=""
|
|
))
|
|
mock_ws.broadcast = AsyncMock()
|
|
mock_db.add_task = MagicMock()
|
|
mock_db.update_task = MagicMock()
|
|
mock_db.get_next_id = MagicMock(return_value=1)
|
|
|
|
response = await client.post(
|
|
"/api/tasks",
|
|
json={"action": "health-check", "host": "test-host"}
|
|
)
|
|
|
|
# Should accept the request
|
|
assert response.status_code in [200, 400, 422]
|
|
|
|
|
|
class TestGetTaskById:
|
|
"""Tests pour GET /api/tasks/{task_id}."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_task_not_found(self, client: AsyncClient):
|
|
"""404 si tâche non trouvée."""
|
|
with patch("app.routes.tasks.db") as mock_db:
|
|
mock_db.get_task.return_value = None
|
|
|
|
response = await client.get("/api/tasks/nonexistent")
|
|
|
|
assert response.status_code == 404
|
|
|
|
|
|
class TestCancelTask:
|
|
"""Tests pour POST /api/tasks/{task_id}/cancel."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cancel_task_not_found(self, client: AsyncClient):
|
|
"""404 si tâche non trouvée."""
|
|
with patch("app.routes.tasks.db") as mock_db:
|
|
mock_db.get_task.return_value = None
|
|
|
|
response = await client.post("/api/tasks/nonexistent/cancel")
|
|
|
|
assert response.status_code == 404
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cancel_task_not_running(
|
|
self, client: AsyncClient, db_session, task_factory
|
|
):
|
|
"""Erreur si tâche pas en cours."""
|
|
task = await task_factory.create(
|
|
db_session,
|
|
id="completed-task",
|
|
action="test",
|
|
status="completed"
|
|
)
|
|
|
|
response = await client.post(f"/api/tasks/{task.id}/cancel")
|
|
|
|
assert response.status_code == 400
|
|
assert "n'est pas en cours" in response.json()["detail"]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cancel_task_success(
|
|
self, client: AsyncClient, db_session, task_factory
|
|
):
|
|
"""Annulation réussie d'une tâche en cours."""
|
|
task = await task_factory.create(
|
|
db_session,
|
|
id="running-cancel-task",
|
|
action="test",
|
|
status="running"
|
|
)
|
|
|
|
with patch("app.routes.tasks.ws_manager") as mock_ws:
|
|
mock_ws.broadcast = AsyncMock()
|
|
|
|
response = await client.post(f"/api/tasks/{task.id}/cancel")
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["success"] is True
|
|
|
|
|
|
class TestGetTaskById:
|
|
"""Tests pour GET /api/tasks/{task_id}."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_task_by_id_success(
|
|
self, client: AsyncClient, db_session, task_factory
|
|
):
|
|
"""Récupère une tâche par ID."""
|
|
task = await task_factory.create(
|
|
db_session,
|
|
id="get-task-id",
|
|
action="test-action",
|
|
target="test-host",
|
|
status="completed"
|
|
)
|
|
|
|
response = await client.get(f"/api/tasks/{task.id}")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["id"] == task.id
|
|
assert data["name"] == "test-action"
|
|
|
|
|
|
class TestDeleteTask:
|
|
"""Tests pour DELETE /api/tasks/{task_id}."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_task_success(
|
|
self, client: AsyncClient, db_session, task_factory
|
|
):
|
|
"""Suppression d'une tâche réussie."""
|
|
task = await task_factory.create(
|
|
db_session,
|
|
id="delete-task-id",
|
|
action="test",
|
|
status="completed"
|
|
)
|
|
|
|
with patch("app.routes.tasks.ws_manager") as mock_ws:
|
|
mock_ws.broadcast = AsyncMock()
|
|
|
|
response = await client.delete(f"/api/tasks/{task.id}")
|
|
|
|
assert response.status_code == 200
|
|
assert "supprimée" in response.json()["message"]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_task_not_found(self, client: AsyncClient):
|
|
"""404 si tâche non trouvée."""
|
|
response = await client.delete("/api/tasks/nonexistent-id")
|
|
|
|
assert response.status_code == 404
|
|
|
|
|
|
class TestTaskLogsDates:
|
|
"""Tests pour GET /api/tasks/logs/dates."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_task_logs_dates(self, client: AsyncClient):
|
|
"""Récupère les dates disponibles."""
|
|
response = await client.get("/api/tasks/logs/dates")
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
class TestTaskLogsStats:
|
|
"""Tests pour GET /api/tasks/logs/stats."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_task_logs_stats(self, client: AsyncClient):
|
|
"""Récupère les statistiques des logs."""
|
|
response = await client.get("/api/tasks/logs/stats")
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
class TestTaskLogContent:
|
|
"""Tests pour GET /api/tasks/logs/{log_id}."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_task_log_content_not_found(self, client: AsyncClient):
|
|
"""404 si log non trouvé."""
|
|
response = await client.get("/api/tasks/logs/nonexistent-log-id")
|
|
|
|
assert response.status_code == 404
|
|
|
|
|
|
class TestDeleteTaskLog:
|
|
"""Tests pour DELETE /api/tasks/logs/{log_id}."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_task_log_not_found(self, client: AsyncClient):
|
|
"""404 si log non trouvé."""
|
|
response = await client.delete("/api/tasks/logs/nonexistent-log-id")
|
|
|
|
assert response.status_code == 404
|
|
|
|
|
|
class TestCreateTask:
|
|
"""Tests supplémentaires pour POST /api/tasks."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_task_with_group(self, client: AsyncClient, db_session):
|
|
"""Création de tâche avec groupe."""
|
|
with patch("app.routes.tasks.ws_manager") as mock_ws:
|
|
mock_ws.broadcast = AsyncMock()
|
|
|
|
response = await client.post(
|
|
"/api/tasks",
|
|
json={
|
|
"action": "health-check",
|
|
"group": "env_prod"
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["host"] == "env_prod"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_task_with_extra_vars(self, client: AsyncClient, db_session):
|
|
"""Création de tâche avec variables extra."""
|
|
with patch("app.routes.tasks.ws_manager") as mock_ws:
|
|
mock_ws.broadcast = AsyncMock()
|
|
|
|
response = await client.post(
|
|
"/api/tasks",
|
|
json={
|
|
"action": "health-check",
|
|
"host": "test-host",
|
|
"extra_vars": {"key": "value"}
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
class TestTaskLogOperations:
|
|
"""Tests pour les opérations sur les logs de tâches."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_task_logs_with_all_filters(self, client: AsyncClient):
|
|
"""Récupère les logs avec tous les filtres."""
|
|
response = await client.get(
|
|
"/api/tasks/logs?status=success&year=2025&month=01&day=15&target=all&limit=10&offset=0"
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "logs" in data
|
|
assert "filters" in data
|
|
assert "pagination" in data
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_task_logs_with_hour_filter(self, client: AsyncClient):
|
|
"""Récupère les logs avec filtre d'heure."""
|
|
response = await client.get(
|
|
"/api/tasks/logs?hour_start=08&hour_end=18"
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_task_logs_with_category(self, client: AsyncClient):
|
|
"""Récupère les logs avec filtre catégorie."""
|
|
response = await client.get("/api/tasks/logs?category=backup")
|
|
|
|
assert response.status_code == 200
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_task_logs_with_source_type(self, client: AsyncClient):
|
|
"""Récupère les logs avec filtre source_type."""
|
|
response = await client.get("/api/tasks/logs?source_type=manual")
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
class TestCancelTaskWithHandle:
|
|
"""Tests pour l'annulation de tâches avec handles."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cancel_task_with_asyncio_task(
|
|
self, client: AsyncClient, db_session, task_factory
|
|
):
|
|
"""Annulation avec asyncio task."""
|
|
task = await task_factory.create(
|
|
db_session,
|
|
id="cancel-asyncio-task",
|
|
action="test",
|
|
status="running"
|
|
)
|
|
|
|
# Mock the running_task_handles
|
|
with patch("app.routes.tasks.running_task_handles", {
|
|
"cancel-asyncio-task": {
|
|
"cancelled": False,
|
|
"asyncio_task": MagicMock(done=MagicMock(return_value=False), cancel=MagicMock())
|
|
}
|
|
}):
|
|
with patch("app.routes.tasks.ws_manager") as mock_ws:
|
|
mock_ws.broadcast = AsyncMock()
|
|
|
|
response = await client.post("/api/tasks/cancel-asyncio-task/cancel")
|
|
|
|
assert response.status_code == 200
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cancel_task_with_process(
|
|
self, client: AsyncClient, db_session, task_factory
|
|
):
|
|
"""Annulation avec process."""
|
|
task = await task_factory.create(
|
|
db_session,
|
|
id="cancel-process-task",
|
|
action="test",
|
|
status="running"
|
|
)
|
|
|
|
mock_process = MagicMock()
|
|
mock_process.returncode = None
|
|
mock_process.terminate = MagicMock()
|
|
mock_process.kill = MagicMock()
|
|
|
|
with patch("app.routes.tasks.running_task_handles", {
|
|
"cancel-process-task": {
|
|
"cancelled": False,
|
|
"process": mock_process
|
|
}
|
|
}):
|
|
with patch("app.routes.tasks.ws_manager") as mock_ws:
|
|
mock_ws.broadcast = AsyncMock()
|
|
with patch("asyncio.sleep", new_callable=AsyncMock):
|
|
response = await client.post("/api/tasks/cancel-process-task/cancel")
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
class TestTaskResponseStructure:
|
|
"""Tests pour la structure des réponses de tâches."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_task_response_has_all_fields(
|
|
self, client: AsyncClient, db_session, task_factory
|
|
):
|
|
"""Vérifie que la réponse contient tous les champs."""
|
|
task = await task_factory.create(
|
|
db_session,
|
|
id="structure-task",
|
|
action="test-action",
|
|
target="test-host",
|
|
status="completed"
|
|
)
|
|
|
|
response = await client.get("/api/tasks/structure-task")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "id" in data
|
|
assert "name" in data
|
|
assert "host" in data
|
|
assert "status" in data
|
|
assert "progress" in data
|
|
assert "start_time" in data
|
|
assert "end_time" in data
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_running_task_progress(
|
|
self, client: AsyncClient, db_session, task_factory
|
|
):
|
|
"""Vérifie le progress pour une tâche running."""
|
|
task = await task_factory.create(
|
|
db_session,
|
|
id="running-progress-task",
|
|
action="test",
|
|
status="running"
|
|
)
|
|
|
|
response = await client.get("/api/tasks/running-progress-task")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["progress"] == 50 # Running = 50%
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_completed_task_progress(
|
|
self, client: AsyncClient, db_session, task_factory
|
|
):
|
|
"""Vérifie le progress pour une tâche completed."""
|
|
task = await task_factory.create(
|
|
db_session,
|
|
id="completed-progress-task",
|
|
action="test",
|
|
status="completed"
|
|
)
|
|
|
|
response = await client.get("/api/tasks/completed-progress-task")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["progress"] == 100 # Completed = 100%
|
|
|
|
|
|
class TestCreateTaskWithPlaybook:
|
|
"""Tests pour création de tâches avec playbook."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_task_triggers_playbook(self, client: AsyncClient, db_session):
|
|
"""Création de tâche déclenche l'exécution du playbook."""
|
|
with patch("app.routes.tasks.ws_manager") as mock_ws, \
|
|
patch("app.routes.tasks.asyncio.create_task") as mock_create_task:
|
|
mock_ws.broadcast = AsyncMock()
|
|
def _create_task_and_close(coro):
|
|
coro.close()
|
|
return MagicMock()
|
|
|
|
mock_create_task.side_effect = _create_task_and_close
|
|
|
|
response = await client.post(
|
|
"/api/tasks",
|
|
json={
|
|
"action": "health-check",
|
|
"host": "all"
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["status"] == "running"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_task_default_target(self, client: AsyncClient, db_session):
|
|
"""Création de tâche avec target par défaut."""
|
|
with patch("app.routes.tasks.ws_manager") as mock_ws, \
|
|
patch("app.routes.tasks.asyncio.create_task") as mock_create_task:
|
|
mock_ws.broadcast = AsyncMock()
|
|
def _create_task_and_close(coro):
|
|
coro.close()
|
|
return MagicMock()
|
|
|
|
mock_create_task.side_effect = _create_task_and_close
|
|
|
|
response = await client.post(
|
|
"/api/tasks",
|
|
json={
|
|
"action": "health-check"
|
|
# No host or group specified
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["host"] == "all" # Default target
|
|
|
|
|
|
class TestDeleteTaskVerify:
|
|
"""Tests pour vérifier la suppression de tâches."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_task_verify_removed(
|
|
self, client: AsyncClient, db_session, task_factory
|
|
):
|
|
"""Vérifie que la tâche est bien supprimée."""
|
|
task = await task_factory.create(
|
|
db_session,
|
|
id="verify-delete-task",
|
|
action="test",
|
|
status="completed"
|
|
)
|
|
|
|
with patch("app.routes.tasks.ws_manager") as mock_ws:
|
|
mock_ws.broadcast = AsyncMock()
|
|
|
|
# Delete
|
|
delete_response = await client.delete("/api/tasks/verify-delete-task")
|
|
assert delete_response.status_code == 200
|
|
|
|
# Verify deleted
|
|
get_response = await client.get("/api/tasks/verify-delete-task")
|
|
assert get_response.status_code == 404
|