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
973 lines
33 KiB
Python
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
|