homelab_automation/tests/backend/test_routes_tasks.py
Bruno Charest 46823eb42d
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
Enhance host status tracking by parsing Ansible PLAY RECAP to update host reachability and last_seen timestamps after health-check playbook executions, add inventory group resolution to host API responses, and trigger automatic data refresh in dashboard after task completion to reflect updated host health indicators
2025-12-22 10:43:17 -05:00

973 lines
33 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
from datetime import datetime
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 TestHostHealthStatusUpdate:
"""Tests du lien health-check -> MAJ status/last_seen host en base."""
async def test_health_check_updates_host_in_db(
self,
async_engine,
db_session,
host_factory,
mock_ws_manager,
mock_notification_service,
):
# Créer un host en base, avec un nom qui correspondra au target
host = await host_factory.create(
db_session,
name="test-host-1.local",
ip_address="192.168.1.10",
status="unknown",
reachable=False,
last_seen=None,
)
# Exécuter le runner directement (le endpoint /api/tasks lance un asyncio.create_task)
from app.routes import tasks as tasks_routes
# Forcer le runner à utiliser le même engine in-memory que le reste des tests
from sqlalchemy.ext.asyncio import async_sessionmaker, AsyncSession
test_session_maker = async_sessionmaker(
async_engine,
class_=AsyncSession,
expire_on_commit=False,
autoflush=False,
)
with patch.object(tasks_routes.ansible_service, "execute_playbook", new=AsyncMock(return_value={
"success": True,
"return_code": 0,
"stdout": "ok",
"stderr": "",
})), patch.object(tasks_routes, "async_session_maker", test_session_maker):
await tasks_routes._execute_task_playbook(
task_id="task-health-1",
task_name="Vérification de santé",
playbook="health-check.yml",
target=host.name,
extra_vars=None,
check_mode=False,
)
# Recharger depuis la DB et vérifier MAJ
from app.crud.host import HostRepository
async with test_session_maker() as verify_session:
repo = HostRepository(verify_session)
updated = await repo.get(host.id)
assert updated is not None
assert updated.status == "online"
assert updated.reachable is True
assert updated.last_seen is not None
assert isinstance(updated.last_seen, datetime)
async def test_health_check_all_updates_each_host_from_recap(
self,
async_engine,
db_session,
host_factory,
mock_ws_manager,
mock_notification_service,
):
host1 = await host_factory.create(
db_session,
name="host1",
ip_address="192.168.1.11",
status="unknown",
reachable=False,
last_seen=None,
)
host2 = await host_factory.create(
db_session,
name="host2",
ip_address="192.168.1.12",
status="unknown",
reachable=False,
last_seen=None,
)
from app.routes import tasks as tasks_routes
from sqlalchemy.ext.asyncio import async_sessionmaker, AsyncSession
test_session_maker = async_sessionmaker(
async_engine,
class_=AsyncSession,
expire_on_commit=False,
autoflush=False,
)
stdout = (
"PLAY RECAP\n"
"host1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0\n"
"host2 : ok=0 changed=0 unreachable=1 failed=0 skipped=0 rescued=0 ignored=0\n"
)
with patch.object(tasks_routes.ansible_service, "execute_playbook", new=AsyncMock(return_value={
"success": False,
"return_code": 2,
"stdout": stdout,
"stderr": "",
})), patch.object(tasks_routes, "async_session_maker", test_session_maker):
await tasks_routes._execute_task_playbook(
task_id="task-health-all-1",
task_name="Vérification de santé",
playbook="health-check.yml",
target="all",
extra_vars=None,
check_mode=False,
)
from app.crud.host import HostRepository
async with test_session_maker() as verify_session:
repo = HostRepository(verify_session)
updated1 = await repo.get(host1.id)
updated2 = await repo.get(host2.id)
assert updated1 is not None
assert updated1.status == "online"
assert updated1.reachable is True
assert updated1.last_seen is not None
assert updated2 is not None
assert updated2.status == "offline"
assert updated2.reachable is False
assert updated2.last_seen is not None
class TestAnsibleExecuteHealthCheckStatusUpdate:
async def test_ansible_execute_health_check_updates_hosts_for_role_group(
self,
client: AsyncClient,
db_session,
host_factory,
):
host1 = await host_factory.create(
db_session,
name="orangepi.pc.home",
ip_address="10.10.0.11",
status="unknown",
reachable=False,
last_seen=None,
)
host2 = await host_factory.create(
db_session,
name="raspi.4gb.home",
ip_address="10.10.0.12",
status="unknown",
reachable=False,
last_seen=None,
)
from app.schemas.host_api import AnsibleInventoryHost
inv_hosts = [
AnsibleInventoryHost(name="orangepi", ansible_host=host1.ip_address, group="env_homelab", groups=["role_sbc", "env_homelab"], vars={}),
AnsibleInventoryHost(name="raspi", ansible_host=host2.ip_address, group="env_homelab", groups=["role_sbc", "env_homelab"], vars={}),
]
stdout = (
"PLAY RECAP\n"
"orangepi : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0\n"
"raspi : ok=0 changed=0 unreachable=1 failed=0 skipped=0 rescued=0 ignored=0\n"
)
with patch("app.routes.ansible.ansible_service.execute_playbook", new=AsyncMock(return_value={
"success": False,
"return_code": 2,
"stdout": stdout,
"stderr": "",
"execution_time": 1.0,
})), patch("app.routes.ansible.ansible_service.get_hosts_from_inventory", new=MagicMock(return_value=inv_hosts)):
resp = await client.post(
"/api/ansible/execute",
json={
"playbook": "health-check.yml",
"target": "role_sbc",
"check_mode": False,
"verbose": True,
},
)
assert resp.status_code == 200
# Verify host statuses are updated via DB session
from app.crud.host import HostRepository
repo = HostRepository(db_session)
updated1 = await repo.get(host1.id)
updated2 = await repo.get(host2.id)
assert updated1 is not None and updated1.status == "online" and updated1.reachable is True and updated1.last_seen is not None
assert updated2 is not None and updated2.status == "offline" and updated2.reachable is False and updated2.last_seen is not None
async def test_health_check_group_updates_each_host_from_recap(
self,
async_engine,
db_session,
host_factory,
mock_ws_manager,
mock_notification_service,
):
host1 = await host_factory.create(
db_session,
name="group-host-1",
ip_address="10.0.0.1",
status="unknown",
reachable=False,
last_seen=None,
)
host2 = await host_factory.create(
db_session,
name="group-host-2",
ip_address="10.0.0.2",
status="unknown",
reachable=False,
last_seen=None,
)
from app.routes import tasks as tasks_routes
from sqlalchemy.ext.asyncio import async_sessionmaker, AsyncSession
test_session_maker = async_sessionmaker(
async_engine,
class_=AsyncSession,
expire_on_commit=False,
autoflush=False,
)
stdout = (
"PLAY RECAP\n"
"group-host-1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0\n"
"group-host-2 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0\n"
)
with patch.object(tasks_routes.ansible_service, "execute_playbook", new=AsyncMock(return_value={
"success": True,
"return_code": 0,
"stdout": stdout,
"stderr": "",
})), patch.object(tasks_routes, "async_session_maker", test_session_maker):
await tasks_routes._execute_task_playbook(
task_id="task-health-group-1",
task_name="Vérification de santé",
playbook="health-check.yml",
target="env_test",
extra_vars=None,
check_mode=False,
)
from app.crud.host import HostRepository
async with test_session_maker() as verify_session:
repo = HostRepository(verify_session)
updated1 = await repo.get(host1.id)
updated2 = await repo.get(host2.id)
assert updated1 is not None and updated1.status == "online" and updated1.reachable is True and updated1.last_seen is not None
assert updated2 is not None and updated2.status == "online" and updated2.reachable is True and updated2.last_seen is not None
async def test_health_check_group_updates_hosts_when_recap_uses_inventory_alias(
self,
async_engine,
db_session,
host_factory,
mock_ws_manager,
mock_notification_service,
):
# DB stores hosts by their FQDN/IP, but Ansible recap may print the inventory alias.
host1 = await host_factory.create(
db_session,
name="sbc-01.local",
ip_address="10.42.0.11",
status="unknown",
reachable=False,
last_seen=None,
)
host2 = await host_factory.create(
db_session,
name="sbc-02.local",
ip_address="10.42.0.12",
status="unknown",
reachable=False,
last_seen=None,
)
from app.routes import tasks as tasks_routes
from sqlalchemy.ext.asyncio import async_sessionmaker, AsyncSession
from app.schemas.host_api import AnsibleInventoryHost
test_session_maker = async_sessionmaker(
async_engine,
class_=AsyncSession,
expire_on_commit=False,
autoflush=False,
)
# Inventory aliases for the role group "role_sbc"
inv_hosts = [
AnsibleInventoryHost(name="sbc-01", ansible_host=host1.ip_address, group="env_test", groups=["role_sbc", "env_test"], vars={}),
AnsibleInventoryHost(name="sbc-02", ansible_host=host2.ip_address, group="env_test", groups=["role_sbc", "env_test"], vars={}),
]
stdout = (
"PLAY RECAP\n"
"sbc-01 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0\n"
"sbc-02 : ok=0 changed=0 unreachable=1 failed=0 skipped=0 rescued=0 ignored=0\n"
)
with patch.object(tasks_routes.ansible_service, "execute_playbook", new=AsyncMock(return_value={
"success": False,
"return_code": 2,
"stdout": stdout,
"stderr": "",
})), patch.object(tasks_routes.ansible_service, "get_hosts_from_inventory", new=MagicMock(return_value=inv_hosts)), patch.object(
tasks_routes,
"async_session_maker",
test_session_maker,
):
await tasks_routes._execute_task_playbook(
task_id="task-health-role-alias-1",
task_name="Vérification de santé",
playbook="health-check.yml",
target="role_sbc",
extra_vars=None,
check_mode=False,
)
from app.crud.host import HostRepository
async with test_session_maker() as verify_session:
repo = HostRepository(verify_session)
updated1 = await repo.get(host1.id)
updated2 = await repo.get(host2.id)
assert updated1 is not None
assert updated1.status == "online"
assert updated1.reachable is True
assert updated1.last_seen is not None
assert updated2 is not None
assert updated2.status == "offline"
assert updated2.reachable is False
assert updated2.last_seen is not None
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