- 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>
297 lines
12 KiB
Python
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}" |