Files
rpa_vision_v3/tests/property/test_vwb_frontend_v2_architecture.py
Dom a27b74cf22 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>
2026-01-29 11:23:51 +01:00

297 lines
12 KiB
Python

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