homelab_automation/tests/backend/test_routes_playbooks.py
Bruno Charest ecefbc8611
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
Clean up test files and debug artifacts, add node_modules to gitignore, export DashboardManager for testing, and enhance pytest configuration with comprehensive test markers and settings
2025-12-15 08:15:49 -05:00

318 lines
13 KiB
Python

"""
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