"""Aucun secret (mot de passe de session, cle API IA) ne doit jamais apparaitre dans un message d'erreur, une exception ou sa representation textuelle - ce sont les seuls endroits, en dehors des fichiers de stockage deja couverts par test_credentials.py/test_ai_settings.py, ou un secret pourrait fuiter sans etre ecrit intentionnellement (une boite de dialogue d'erreur, une trace affichee a l'utilisateur ou capturee dans un rapport de bug).""" import sys, pathlib sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent)) import io import urllib.error from unittest.mock import patch, MagicMock from app.ai.provider import OpenAICompatibleProvider, AIError SECRET_API_KEY = "sk-tres-secret-ne-jamais-fuiter-1234567890" def make_http_error(code: int, body: bytes = b"") -> urllib.error.HTTPError: return urllib.error.HTTPError(url="http://x/chat/completions", code=code, msg="err", hdrs=None, fp=io.BytesIO(body)) provider = OpenAICompatibleProvider( base_url="https://api.example.com/v1", api_key=SECRET_API_KEY, model="gpt-test", ) print("=== 1. Erreur 401 : le message ne contient jamais la cle API ===") with patch("app.ai.provider.urllib.request.urlopen", side_effect=make_http_error(401, b"unauthorized")): try: provider.chat([{"role": "user", "content": "x"}]) raise AssertionError("aurait du lever AIError") except AIError as exc: message = str(exc) print(" message:", message) assert SECRET_API_KEY not in message print(" OK") print() print("=== 2. Erreur 500 avec corps de reponse : la cle API n'y apparait jamais ===") # le corps de la reponse est celui du FOURNISSEUR (pas la requete) : il ne peut # pas contenir la cle, mais on verifie que rien ne l'y ajoute par erreur. with patch("app.ai.provider.urllib.request.urlopen", side_effect=make_http_error(500, b"internal error")): try: provider.chat([{"role": "user", "content": "x"}]) raise AssertionError("aurait du lever AIError") except AIError as exc: message = str(exc) print(" message:", message) assert SECRET_API_KEY not in message print(" OK") print() print("=== 3. Erreur reseau : le message ne contient jamais la cle API ===") with patch("app.ai.provider.urllib.request.urlopen", side_effect=urllib.error.URLError("connexion refusee")): try: provider.chat([{"role": "user", "content": "x"}]) raise AssertionError("aurait du lever AIError") except AIError as exc: message = str(exc) print(" message:", message) assert SECRET_API_KEY not in message print(" OK") print() print("=== 4. Reponse invalide (JSON incoherent) : le message ne contient jamais la cle API ===") bad_response = MagicMock() bad_response.read.return_value = b"{\"unexpected\": \"shape\"}" bad_response.__enter__.return_value = bad_response bad_response.__exit__.return_value = False with patch("app.ai.provider.urllib.request.urlopen", return_value=bad_response): try: provider.chat([{"role": "user", "content": "x"}]) raise AssertionError("aurait du lever AIError") except AIError as exc: message = str(exc) print(" message:", message) assert SECRET_API_KEY not in message print(" OK") print() print("=== 5. La cle API n'apparait pas non plus dans repr(provider) ===") print(" repr:", repr(provider)) assert SECRET_API_KEY not in repr(provider) assert SECRET_API_KEY not in str(provider) print(" OK") print() print("NO SECRETS IN ERRORS : TOUS LES TESTS OK")