v1.0 - Version stable: multi-PC, détection UI-DETR-1, 3 modes exécution
- Frontend v4 accessible sur réseau local (192.168.1.40) - Ports ouverts: 3002 (frontend), 5001 (backend), 5004 (dashboard) - Ollama GPU fonctionnel - Self-healing interactif - Dashboard confiance Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
297
tests/property/test_vwb_frontend_v2_architecture.py
Normal file
297
tests/property/test_vwb_frontend_v2_architecture.py
Normal file
@@ -0,0 +1,297 @@
|
||||
"""
|
||||
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}"
|
||||
Reference in New Issue
Block a user