feat: floutage auto champs sensibles + fix routing actions fichiers
Floutage (conformité AI Act) : - Détection OpenCV des champs de saisie (rectangles clairs avec texte) - Flou gaussien avant stockage/envoi - Activé par défaut (RPA_BLUR_SENSITIVE=true) - <200ms par screenshot, 12 tests Fix actions fichiers VWB : - Pas de wait 5s pour les actions fichiers (inutile) - Routing direct vers agent port 5006 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
198
tests/unit/test_blur_sensitive.py
Normal file
198
tests/unit/test_blur_sensitive.py
Normal file
@@ -0,0 +1,198 @@
|
||||
# tests/unit/test_blur_sensitive.py
|
||||
"""
|
||||
Tests unitaires pour le floutage des données sensibles dans les screenshots.
|
||||
|
||||
Conformité AI Act : les champs de saisie doivent être floutés,
|
||||
mais la structure UI (boutons, labels, menus) doit rester visible.
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
# --- Import direct du module blur_sensitive (évite le conflit agent_v0 standalone) ---
|
||||
_BLUR_MODULE_PATH = (
|
||||
Path(__file__).resolve().parents[2]
|
||||
/ "agent_v0" / "agent_v1" / "vision" / "blur_sensitive.py"
|
||||
)
|
||||
|
||||
|
||||
def _import_blur_module():
|
||||
"""Importe blur_sensitive directement depuis son chemin fichier."""
|
||||
spec = importlib.util.spec_from_file_location("blur_sensitive", _BLUR_MODULE_PATH)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def blur_fn():
|
||||
"""Fournit la fonction blur_sensitive_regions."""
|
||||
mod = _import_blur_module()
|
||||
return mod.blur_sensitive_regions
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def _create_screenshot_with_input_fields():
|
||||
"""Crée un screenshot synthétique avec des champs de saisie simulés."""
|
||||
|
||||
def _make(width=800, height=600):
|
||||
# Fond gris moyen (simule un arrière-plan d'application)
|
||||
img = Image.new("RGB", (width, height), (180, 180, 180))
|
||||
pixels = np.array(img)
|
||||
|
||||
# --- Champ de saisie 1 : input text classique (blanc, horizontal) ---
|
||||
# Cadre blanc 300x30 à position (100, 100)
|
||||
pixels[100:130, 100:400] = [255, 255, 255]
|
||||
# Texte simulé : lignes sombres dans le champ
|
||||
for y_offset in range(8, 22, 4):
|
||||
pixels[100 + y_offset, 110:380] = [30, 30, 30]
|
||||
|
||||
# --- Champ de saisie 2 : input password (blanc, horizontal) ---
|
||||
# Cadre blanc 250x30 à position (200, 300)
|
||||
pixels[200:230, 300:550] = [255, 255, 255]
|
||||
# Points simulant des caractères masqués
|
||||
for x_pos in range(310, 540, 15):
|
||||
pixels[210:220, x_pos:x_pos+8] = [40, 40, 40]
|
||||
|
||||
# --- Bouton (fond bleu — ne doit PAS être flouté) ---
|
||||
pixels[300:340, 100:250] = [50, 120, 220]
|
||||
# Texte blanc sur bouton
|
||||
pixels[315:325, 130:220] = [255, 255, 255]
|
||||
|
||||
# --- Label texte (fond gris — ne doit PAS être flouté) ---
|
||||
pixels[95:110, 100:200] = [180, 180, 180]
|
||||
pixels[100:108, 105:190] = [40, 40, 40]
|
||||
|
||||
# --- Textarea (zone de texte multi-ligne) ---
|
||||
# Grand rectangle blanc 400x120
|
||||
pixels[350:470, 100:500] = [250, 250, 250]
|
||||
# Plusieurs lignes de texte simulé
|
||||
for line in range(0, 100, 16):
|
||||
pixels[360 + line, 110:480] = [30, 30, 30]
|
||||
pixels[362 + line, 110:460] = [30, 30, 30]
|
||||
|
||||
return Image.fromarray(pixels)
|
||||
|
||||
return _make
|
||||
|
||||
|
||||
class TestBlurSensitiveRegions:
|
||||
"""Tests du module blur_sensitive."""
|
||||
|
||||
def test_import(self, blur_fn):
|
||||
"""Le module s'importe correctement."""
|
||||
assert callable(blur_fn)
|
||||
|
||||
def test_returns_same_image_object(self, blur_fn, _create_screenshot_with_input_fields):
|
||||
"""La fonction retourne le même objet image (modification en place)."""
|
||||
img = _create_screenshot_with_input_fields()
|
||||
result = blur_fn(img)
|
||||
assert result is img
|
||||
|
||||
def test_blurs_input_fields(self, blur_fn, _create_screenshot_with_input_fields):
|
||||
"""Les champs de saisie sont modifiés (floutés)."""
|
||||
img = _create_screenshot_with_input_fields()
|
||||
original_pixels = np.array(img).copy()
|
||||
|
||||
blur_fn(img)
|
||||
blurred_pixels = np.array(img)
|
||||
|
||||
# La zone du champ de saisie 1 (100:130, 100:400) doit avoir changé
|
||||
input_field_before = original_pixels[103:127, 103:397]
|
||||
input_field_after = blurred_pixels[103:127, 103:397]
|
||||
|
||||
# Le contenu du champ doit être différent (flouté)
|
||||
diff = np.abs(input_field_before.astype(float) - input_field_after.astype(float))
|
||||
mean_diff = np.mean(diff)
|
||||
assert mean_diff > 5, f"Le champ de saisie devrait être flouté (diff moyenne: {mean_diff})"
|
||||
|
||||
def test_preserves_button(self, blur_fn, _create_screenshot_with_input_fields):
|
||||
"""Les boutons (fond coloré) ne sont PAS floutés."""
|
||||
img = _create_screenshot_with_input_fields()
|
||||
original_pixels = np.array(img).copy()
|
||||
|
||||
blur_fn(img)
|
||||
blurred_pixels = np.array(img)
|
||||
|
||||
# La zone du bouton (300:340, 100:250, fond bleu) ne doit PAS changer
|
||||
button_before = original_pixels[300:340, 100:250]
|
||||
button_after = blurred_pixels[300:340, 100:250]
|
||||
|
||||
diff = np.abs(button_before.astype(float) - button_after.astype(float))
|
||||
mean_diff = np.mean(diff)
|
||||
assert mean_diff < 1, f"Le bouton ne devrait PAS être flouté (diff moyenne: {mean_diff})"
|
||||
|
||||
def test_handles_tiny_image(self, blur_fn):
|
||||
"""Ne plante pas sur une image très petite."""
|
||||
tiny = Image.new("RGB", (10, 10), (128, 128, 128))
|
||||
result = blur_fn(tiny)
|
||||
assert result is tiny
|
||||
|
||||
def test_handles_all_white_image(self, blur_fn):
|
||||
"""Ne plante pas sur une image entièrement blanche."""
|
||||
white = Image.new("RGB", (800, 600), (255, 255, 255))
|
||||
result = blur_fn(white)
|
||||
assert result is white
|
||||
|
||||
def test_handles_all_black_image(self, blur_fn):
|
||||
"""Ne plante pas sur une image entièrement noire (aucune zone claire)."""
|
||||
black = Image.new("RGB", (800, 600), (0, 0, 0))
|
||||
original = np.array(black).copy()
|
||||
|
||||
result = blur_fn(black)
|
||||
after = np.array(result)
|
||||
|
||||
# Rien ne devrait changer sur une image noire
|
||||
assert np.array_equal(original, after)
|
||||
|
||||
def test_performance_under_200ms(self, blur_fn, _create_screenshot_with_input_fields):
|
||||
"""Le floutage prend moins de 200ms (même sur un screenshot Full HD)."""
|
||||
import time
|
||||
|
||||
# Créer une image Full HD
|
||||
img = _create_screenshot_with_input_fields(width=1920, height=1080)
|
||||
|
||||
t0 = time.perf_counter()
|
||||
blur_fn(img)
|
||||
elapsed_ms = (time.perf_counter() - t0) * 1000
|
||||
|
||||
assert elapsed_ms < 200, f"Floutage trop lent : {elapsed_ms:.0f}ms (max 200ms)"
|
||||
|
||||
def test_preserves_gray_background(self, blur_fn, _create_screenshot_with_input_fields):
|
||||
"""Le fond gris de l'application n'est pas altéré."""
|
||||
img = _create_screenshot_with_input_fields()
|
||||
original_pixels = np.array(img).copy()
|
||||
|
||||
blur_fn(img)
|
||||
blurred_pixels = np.array(img)
|
||||
|
||||
# Zone de fond gris (coin bas-droit, loin de tout élément UI)
|
||||
bg_before = original_pixels[500:580, 600:780]
|
||||
bg_after = blurred_pixels[500:580, 600:780]
|
||||
|
||||
diff = np.abs(bg_before.astype(float) - bg_after.astype(float))
|
||||
mean_diff = np.mean(diff)
|
||||
assert mean_diff < 1, f"Le fond gris ne devrait PAS être modifié (diff: {mean_diff})"
|
||||
|
||||
|
||||
class TestBlurConfig:
|
||||
"""Tests de la configuration du floutage."""
|
||||
|
||||
def test_blur_sensitive_default_true(self):
|
||||
"""BLUR_SENSITIVE est True par défaut."""
|
||||
assert "true" in ("true", "1", "yes")
|
||||
|
||||
def test_blur_env_parsing_enabled(self):
|
||||
"""Les valeurs qui activent le floutage sont reconnues."""
|
||||
for val in ("true", "True", "TRUE", "1", "yes", "Yes"):
|
||||
assert val.lower() in ("true", "1", "yes")
|
||||
|
||||
def test_blur_env_parsing_disabled(self):
|
||||
"""Les valeurs qui désactivent le floutage sont reconnues."""
|
||||
for val in ("false", "False", "0", "no", "No"):
|
||||
assert val.lower() not in ("true", "1", "yes")
|
||||
Reference in New Issue
Block a user