""" Tests de propriété pour l'architecture du Frontend Visual Workflow Builder V2 Auteur : Dom, Alice, Kiro - 08 janvier 2026 Tests property-based pour valider l'intégration API REST et la cohérence architecturale. Propriété 32 : Intégration API REST - Pour toute opération CRUD, l'API REST du Backend_VWB doit être utilisée avec gestion d'erreurs et système de retry. """ import pytest import requests import json import time from hypothesis import given, strategies as st, settings from unittest.mock import Mock, patch, MagicMock from typing import Dict, Any, List class TestVWBFrontendArchitecture: """Tests de propriété pour l'architecture frontend""" def setup_method(self): """Configuration avant chaque test""" self.base_url = "http://localhost:5000/api" self.timeout = 5 self.max_retries = 3 @given( workflow_data=st.dictionaries( keys=st.text(min_size=1, max_size=50), values=st.one_of( st.text(min_size=1, max_size=100), st.integers(min_value=0, max_value=1000), st.booleans(), st.lists(st.text(min_size=1, max_size=20), min_size=0, max_size=10) ), min_size=1, max_size=10 ) ) @settings(max_examples=100, deadline=5000) def test_api_rest_crud_operations_property(self, workflow_data: Dict[str, Any]): """ Feature: visual-workflow-builder-frontend-v2, Property 32: Intégration API REST Pour toute opération CRUD, l'API REST du Backend_VWB doit être utilisée avec gestion d'erreurs et système de retry. """ # Simuler les appels API avec mock with patch('requests.post') as mock_post, \ patch('requests.get') as mock_get, \ patch('requests.put') as mock_put, \ patch('requests.delete') as mock_delete: # Configuration des mocks pour simuler des réponses réussies mock_response = Mock() mock_response.status_code = 200 mock_response.json.return_value = {"id": "test_id", "status": "success"} mock_post.return_value = mock_response mock_get.return_value = mock_response mock_put.return_value = mock_response mock_delete.return_value = mock_response # Test CREATE (POST) create_result = self._simulate_api_call('POST', '/workflows', workflow_data) assert create_result['success'] is True mock_post.assert_called_once() # Test READ (GET) read_result = self._simulate_api_call('GET', '/workflows/test_id') assert read_result['success'] is True mock_get.assert_called_once() # Test UPDATE (PUT) update_result = self._simulate_api_call('PUT', '/workflows/test_id', workflow_data) assert update_result['success'] is True mock_put.assert_called_once() # Test DELETE delete_result = self._simulate_api_call('DELETE', '/workflows/test_id') assert delete_result['success'] is True mock_delete.assert_called_once() @given( retry_count=st.integers(min_value=1, max_value=5), error_codes=st.lists( st.sampled_from([500, 502, 503, 504, 408, 429]), min_size=1, max_size=3 ) ) @settings(max_examples=50, deadline=3000) def test_api_retry_system_property(self, retry_count: int, error_codes: List[int]): """ Feature: visual-workflow-builder-frontend-v2, Property 32: Système de retry Pour toute requête échouée, un système de retry doit être implémenté avec gestion appropriée des erreurs temporaires. """ with patch('requests.post') as mock_post: # Simuler des échecs suivis d'un succès error_responses = [] for code in error_codes[:retry_count-1]: error_response = Mock() error_response.status_code = code error_response.raise_for_status.side_effect = requests.exceptions.HTTPError() error_responses.append(error_response) # Réponse de succès finale success_response = Mock() success_response.status_code = 200 success_response.json.return_value = {"status": "success"} error_responses.append(success_response) mock_post.side_effect = error_responses # Tester le système de retry result = self._simulate_api_call_with_retry('POST', '/workflows', {}, max_retries=retry_count) # Vérifier que le nombre d'appels correspond aux tentatives assert mock_post.call_count == len(error_responses) assert result['success'] is True @given( validation_data=st.dictionaries( keys=st.sampled_from(['name', 'type', 'parameters', 'connections']), values=st.one_of( st.text(min_size=0, max_size=100), st.none(), st.integers(), st.lists(st.text(), min_size=0, max_size=5) ), min_size=1, max_size=4 ) ) @settings(max_examples=100, deadline=3000) def test_client_side_validation_property(self, validation_data: Dict[str, Any]): """ Feature: visual-workflow-builder-frontend-v2, Property 33: Validation Côté Client Pour toute donnée envoyée au backend, elle doit être validée côté client avant transmission. """ # Simuler la validation côté client validation_result = self._validate_client_side(validation_data) # Si les données sont valides, elles peuvent être envoyées if validation_result['is_valid']: with patch('requests.post') as mock_post: mock_response = Mock() mock_response.status_code = 200 mock_response.json.return_value = {"status": "success"} mock_post.return_value = mock_response # Les données valides doivent être envoyées au backend result = self._simulate_api_call('POST', '/workflows', validation_data) assert result['success'] is True mock_post.assert_called_once() else: # Les données invalides ne doivent pas être envoyées with patch('requests.post') as mock_post: result = self._simulate_api_call('POST', '/workflows', validation_data) assert result['success'] is False assert 'validation_errors' in result # Vérifier qu'aucun appel API n'a été fait mock_post.assert_not_called() def _simulate_api_call(self, method: str, endpoint: str, data: Dict[str, Any] = None) -> Dict[str, Any]: """Simuler un appel API avec gestion d'erreurs""" # Validation côté client avant l'appel API if data is not None: validation_result = self._validate_client_side(data) if not validation_result['is_valid']: return { 'success': False, 'error': 'Validation échouée', 'validation_errors': validation_result['errors'] } try: url = f"{self.base_url}{endpoint}" if method == 'POST': response = requests.post(url, json=data, timeout=self.timeout) elif method == 'GET': response = requests.get(url, timeout=self.timeout) elif method == 'PUT': response = requests.put(url, json=data, timeout=self.timeout) elif method == 'DELETE': response = requests.delete(url, timeout=self.timeout) else: return {'success': False, 'error': 'Méthode non supportée'} response.raise_for_status() return {'success': True, 'data': response.json()} except requests.exceptions.RequestException as e: return {'success': False, 'error': str(e)} def _simulate_api_call_with_retry(self, method: str, endpoint: str, data: Dict[str, Any] = None, max_retries: int = 3) -> Dict[str, Any]: """Simuler un appel API avec système de retry""" last_error = None for attempt in range(max_retries): try: result = self._simulate_api_call(method, endpoint, data) if result['success']: return result last_error = result.get('error', 'Erreur inconnue') except Exception as e: last_error = str(e) # Attendre avant la prochaine tentative (sauf pour le dernier essai) if attempt < max_retries - 1: time.sleep(0.01) # Réduire le délai pour les tests return {'success': False, 'error': f'Échec après {max_retries} tentatives: {last_error}'} def _validate_client_side(self, data: Dict[str, Any]) -> Dict[str, Any]: """Simuler la validation côté client""" errors = [] # Règles de validation basiques if 'name' in data: if not data['name'] or (isinstance(data['name'], str) and len(data['name'].strip()) == 0): errors.append("Le nom est obligatoire") if 'type' in data: valid_types = ['click', 'type', 'wait', 'condition', 'extract'] if data['type'] not in valid_types: errors.append(f"Type invalide. Types valides: {valid_types}") if 'parameters' in data and data['parameters'] is not None: if not isinstance(data['parameters'], (dict, list)): errors.append("Les paramètres doivent être un objet ou une liste") is_valid = len(errors) == 0 result = { 'is_valid': is_valid, 'errors': errors } # Si invalide, ajouter les erreurs de validation au résultat if not is_valid: result['validation_errors'] = errors return result @given( error_scenarios=st.lists( st.dictionaries( keys=st.sampled_from(['status_code', 'error_type', 'message']), values=st.one_of( st.integers(min_value=400, max_value=599), st.sampled_from(['timeout', 'connection', 'server_error']), st.text(min_size=1, max_size=100) ), min_size=1, max_size=3 ), min_size=1, max_size=3 ) ) @settings(max_examples=50, deadline=3000) def test_error_handling_graceful_property(self, error_scenarios: List[Dict[str, Any]]): """ Feature: visual-workflow-builder-frontend-v2, Property 32: Gestion d'erreurs gracieuse Pour toute erreur de communication backend, elle doit être gérée gracieusement avec messages utilisateur appropriés. """ for scenario in error_scenarios: with patch('requests.post') as mock_post: # Simuler différents types d'erreurs error_type = scenario.get('error_type', 'server_error') if error_type == 'timeout': mock_post.side_effect = requests.exceptions.Timeout("Timeout simulé") elif error_type == 'connection': mock_post.side_effect = requests.exceptions.ConnectionError("Erreur de connexion simulée") else: error_response = Mock() error_response.status_code = scenario.get('status_code', 500) error_response.raise_for_status.side_effect = requests.exceptions.HTTPError("Erreur HTTP simulée") mock_post.return_value = error_response # Tester la gestion d'erreur result = self._simulate_api_call('POST', '/workflows', {'test': 'data'}) # Vérifier que l'erreur est gérée gracieusement assert result['success'] is False assert 'error' in result assert isinstance(result['error'], str) assert len(result['error']) > 0, f"Message d'erreur vide pour le scénario: {scenario}"