""" Tests pour les routes de gestion des playbooks. Couvre: - Lecture du contenu d'un playbook - Sauvegarde du contenu d'un playbook - Validation YAML - Sécurité des chemins """ import pytest from unittest.mock import patch, MagicMock from pathlib import Path from httpx import AsyncClient pytestmark = pytest.mark.unit class TestGetPlaybookContent: """Tests pour GET /api/playbooks/{filename}/content.""" async def test_get_playbook_content_success(self, client: AsyncClient, tmp_path): """Lecture du contenu d'un playbook.""" playbook_content = "---\n- name: Test\n hosts: all\n tasks: []" playbook_file = tmp_path / "test.yml" playbook_file.write_text(playbook_content) with patch("app.routes.playbooks.ansible_service") as mock_ansible: mock_ansible.playbooks_dir = tmp_path response = await client.get("/api/playbooks/test.yml/content") assert response.status_code == 200 data = response.json() assert data["filename"] == "test.yml" assert data["content"] == playbook_content assert "size" in data assert "modified" in data async def test_get_playbook_invalid_extension(self, client: AsyncClient): """Erreur si extension invalide.""" with patch("app.routes.playbooks.ansible_service") as mock_ansible: mock_ansible.playbooks_dir = Path("/tmp") response = await client.get("/api/playbooks/test.txt/content") assert response.status_code == 400 assert "Extension" in response.json()["detail"] async def test_get_playbook_not_found(self, client: AsyncClient, tmp_path): """Erreur si playbook non trouvé.""" with patch("app.routes.playbooks.ansible_service") as mock_ansible: mock_ansible.playbooks_dir = tmp_path response = await client.get("/api/playbooks/nonexistent.yml/content") assert response.status_code == 404 async def test_get_playbook_path_traversal_blocked(self, client: AsyncClient, tmp_path): """Bloque les tentatives de path traversal.""" with patch("app.routes.playbooks.ansible_service") as mock_ansible: mock_ansible.playbooks_dir = tmp_path # Create a file outside playbooks dir (tmp_path.parent / "secret.yml").write_text("secret") response = await client.get("/api/playbooks/../secret.yml/content") # Should be blocked (404 or 403) assert response.status_code in [403, 404] class TestSavePlaybookContent: """Tests pour PUT /api/playbooks/{filename}/content.""" async def test_save_playbook_success(self, client: AsyncClient, tmp_path): """Sauvegarde du contenu d'un playbook.""" with patch("app.routes.playbooks.ansible_service") as mock_ansible, \ patch("app.routes.playbooks.db") as mock_db: mock_ansible.playbooks_dir = tmp_path mock_db.get_next_id.return_value = 1 mock_db.logs = [] response = await client.put( "/api/playbooks/new-playbook.yml/content", json={"content": "---\n- name: New\n hosts: all\n tasks: []"} ) assert response.status_code == 200 data = response.json() assert data["success"] is True assert "créé" in data["message"] async def test_save_playbook_update_existing(self, client: AsyncClient, tmp_path): """Mise à jour d'un playbook existant.""" existing = tmp_path / "existing.yml" existing.write_text("---\n- name: Old\n hosts: all") with patch("app.routes.playbooks.ansible_service") as mock_ansible, \ patch("app.routes.playbooks.db") as mock_db: mock_ansible.playbooks_dir = tmp_path mock_db.get_next_id.return_value = 1 mock_db.logs = [] response = await client.put( "/api/playbooks/existing.yml/content", json={"content": "---\n- name: Updated\n hosts: all\n tasks: []"} ) assert response.status_code == 200 data = response.json() assert data["success"] is True assert "sauvegardé" in data["message"] async def test_save_playbook_invalid_extension(self, client: AsyncClient): """Erreur si extension invalide.""" response = await client.put( "/api/playbooks/test.txt/content", json={"content": "test"} ) assert response.status_code == 400 async def test_save_playbook_invalid_filename(self, client: AsyncClient): """Erreur si nom de fichier invalide.""" response = await client.put( "/api/playbooks/../../etc/passwd.yml/content", json={"content": "---\n- name: Test"} ) # Should be blocked (400 or 404) assert response.status_code in [400, 404] async def test_save_playbook_invalid_yaml(self, client: AsyncClient, tmp_path): """Erreur si YAML invalide.""" with patch("app.routes.playbooks.ansible_service") as mock_ansible: mock_ansible.playbooks_dir = tmp_path response = await client.put( "/api/playbooks/invalid.yml/content", json={"content": "not: valid: yaml: ["} ) assert response.status_code == 400 assert "YAML" in response.json()["detail"] async def test_save_playbook_empty_yaml(self, client: AsyncClient, tmp_path): """Erreur si YAML vide.""" with patch("app.routes.playbooks.ansible_service") as mock_ansible: mock_ansible.playbooks_dir = tmp_path response = await client.put( "/api/playbooks/empty.yml/content", json={"content": ""} ) assert response.status_code == 400 class TestDeletePlaybook: """Tests pour DELETE /api/playbooks/{filename}.""" async def test_delete_playbook_success(self, client: AsyncClient, tmp_path): """Suppression d'un playbook.""" playbook_file = tmp_path / "to-delete.yml" playbook_file.write_text("---\n- name: Test\n hosts: all") with patch("app.routes.playbooks.ansible_service") as mock_ansible, \ patch("app.routes.playbooks.db") as mock_db: mock_ansible.playbooks_dir = tmp_path mock_db.get_next_id.return_value = 1 mock_db.logs = [] response = await client.delete("/api/playbooks/to-delete.yml") assert response.status_code == 200 data = response.json() assert data["success"] is True assert "supprimé" in data["message"] assert not playbook_file.exists() async def test_delete_playbook_invalid_extension(self, client: AsyncClient): """Erreur si extension invalide.""" response = await client.delete("/api/playbooks/test.txt") assert response.status_code == 400 assert "Extension" in response.json()["detail"] async def test_delete_playbook_not_found(self, client: AsyncClient, tmp_path): """Erreur si playbook non trouvé.""" with patch("app.routes.playbooks.ansible_service") as mock_ansible: mock_ansible.playbooks_dir = tmp_path response = await client.delete("/api/playbooks/nonexistent.yml") assert response.status_code == 404 async def test_delete_playbook_path_traversal_blocked(self, client: AsyncClient, tmp_path): """Bloque les tentatives de path traversal lors de la suppression.""" # Create a file outside playbooks dir secret_file = tmp_path.parent / "secret.yml" secret_file.write_text("secret content") with patch("app.routes.playbooks.ansible_service") as mock_ansible: mock_ansible.playbooks_dir = tmp_path response = await client.delete("/api/playbooks/../secret.yml") # Should be blocked (403 or 404) assert response.status_code in [403, 404] # File should still exist assert secret_file.exists() class TestPlaybookContentReadError: """Tests pour les erreurs de lecture de playbook.""" async def test_get_playbook_read_error(self, client: AsyncClient, tmp_path): """Erreur lors de la lecture du fichier.""" playbook_file = tmp_path / "unreadable.yml" playbook_file.write_text("content") with patch("app.routes.playbooks.ansible_service") as mock_ansible: mock_ansible.playbooks_dir = tmp_path # Mock read_text to raise an exception with patch.object(Path, 'read_text', side_effect=PermissionError("Access denied")): response = await client.get("/api/playbooks/unreadable.yml/content") assert response.status_code == 500 assert "Erreur lecture" in response.json()["detail"] class TestPlaybookContentWriteError: """Tests pour les erreurs d'écriture de playbook.""" async def test_save_playbook_write_error(self, client: AsyncClient, tmp_path): """Erreur lors de l'écriture du fichier.""" with patch("app.routes.playbooks.ansible_service") as mock_ansible: mock_ansible.playbooks_dir = tmp_path # Mock write_text to raise an exception with patch.object(Path, 'write_text', side_effect=PermissionError("Access denied")): response = await client.put( "/api/playbooks/error.yml/content", json={"content": "---\n- name: Test\n hosts: all"} ) assert response.status_code == 500 assert "Erreur sauvegarde" in response.json()["detail"] class TestPlaybookDeleteError: """Tests pour les erreurs de suppression de playbook.""" async def test_delete_playbook_unlink_error(self, client: AsyncClient, tmp_path): """Erreur lors de la suppression du fichier.""" playbook_file = tmp_path / "error-delete.yml" playbook_file.write_text("content") with patch("app.routes.playbooks.ansible_service") as mock_ansible: mock_ansible.playbooks_dir = tmp_path # Mock unlink to raise an exception with patch.object(Path, 'unlink', side_effect=PermissionError("Access denied")): response = await client.delete("/api/playbooks/error-delete.yml") assert response.status_code == 500 assert "Erreur suppression" in response.json()["detail"] class TestPlaybookYamlExtension: """Tests pour les extensions .yaml.""" async def test_get_playbook_yaml_extension(self, client: AsyncClient, tmp_path): """Lecture d'un playbook avec extension .yaml.""" playbook_content = "---\n- name: Test\n hosts: all" playbook_file = tmp_path / "test.yaml" playbook_file.write_text(playbook_content) with patch("app.routes.playbooks.ansible_service") as mock_ansible: mock_ansible.playbooks_dir = tmp_path response = await client.get("/api/playbooks/test.yaml/content") assert response.status_code == 200 assert response.json()["filename"] == "test.yaml" async def test_save_playbook_yaml_extension(self, client: AsyncClient, tmp_path): """Sauvegarde d'un playbook avec extension .yaml.""" with patch("app.routes.playbooks.ansible_service") as mock_ansible, \ patch("app.routes.playbooks.db") as mock_db: mock_ansible.playbooks_dir = tmp_path mock_db.get_next_id.return_value = 1 mock_db.logs = [] response = await client.put( "/api/playbooks/new-playbook.yaml/content", json={"content": "---\n- name: New\n hosts: all"} ) assert response.status_code == 200 async def test_delete_playbook_yaml_extension(self, client: AsyncClient, tmp_path): """Suppression d'un playbook avec extension .yaml.""" playbook_file = tmp_path / "delete.yaml" playbook_file.write_text("---\n- name: Test") with patch("app.routes.playbooks.ansible_service") as mock_ansible, \ patch("app.routes.playbooks.db") as mock_db: mock_ansible.playbooks_dir = tmp_path mock_db.get_next_id.return_value = 1 mock_db.logs = [] response = await client.delete("/api/playbooks/delete.yaml") assert response.status_code == 200