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