Files
rpa_vision_v3/tests/unit/test_blur_sensitive.py
Dom 353c2a347e 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>
2026-03-18 16:24:01 +01:00

199 lines
7.4 KiB
Python

# 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")