""" 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() mock_create_task.return_value = MagicMock() 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: mock_ws.broadcast = AsyncMock() 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