"""Credential Vault : abstraction CredentialStore, backend Windows Credential Manager reel (nettoye apres coup), et integration avec les flux de mot de passe (ConnectionController, bastion, suppression de session). SSHConnection.start() est neutralisee pour les tests d'integration qui construisent un onglet reel (aucune connexion reseau necessaire).""" import sys, pathlib sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent)) import json import tempfile from pathlib import Path from unittest.mock import patch from PySide6.QtWidgets import QApplication, QWidget, QMessageBox from app.credentials import ( CredentialStore, NullCredentialStore, WindowsCredentialStore, session_credential_key, default_credential_store, ) from app.session_manager import Session, SessionStore, AUTH_PASSWORD, PASSWORD_STORAGE_SECURE from app.ssh_connection import SSHConnection from app.controllers.broadcast_manager import BroadcastManager from app.controllers.connection_controller import ConnectionController from app.controllers.session_controller import SessionController from app.bastion import BastionTarget app = QApplication.instance() or QApplication([]) tmp = Path(tempfile.mkdtemp()) no_ssh_thread = patch.object(SSHConnection, "start", lambda self: None) no_ssh_thread.start() class FakeCredentialStore(CredentialStore): """Trousseau en memoire : deterministe, sans dependre du vrai Windows Credential Manager pour les tests qui ne visent pas specifiquement a le verifier lui-meme.""" def __init__(self): self._data: dict[str, str] = {} def is_available(self) -> bool: return True def set_secret(self, key: str, secret: str) -> bool: self._data[key] = secret return True def get_secret(self, key: str): return self._data.get(key) def delete_secret(self, key: str) -> None: self._data.pop(key, None) print("=== 1. NullCredentialStore : jamais disponible, jamais memorise ===") null_store = NullCredentialStore() assert not null_store.is_available() assert null_store.set_secret("x", "y") is False assert null_store.get_secret("x") is None null_store.delete_secret("x") # ne doit pas lever print(" OK") print() print("=== 2. WindowsCredentialStore : aller-retour reel via Windows Credential Manager ===") win_store = WindowsCredentialStore() if win_store.is_available(): test_key = "test-vault-phase11" win_store.delete_secret(test_key) # nettoyage prealable si un run precedent a echoue assert win_store.get_secret(test_key) is None ok = win_store.set_secret(test_key, "s3cret-valeur-de-test") assert ok assert win_store.get_secret(test_key) == "s3cret-valeur-de-test" win_store.delete_secret(test_key) assert win_store.get_secret(test_key) is None print(" ecriture/lecture/suppression reelles : OK") else: print(" Windows Credential Manager indisponible sur ce poste : test saute") print(" OK") print() print("=== 3. default_credential_store() ne leve jamais, meme si indisponible ===") store = default_credential_store() assert isinstance(store, CredentialStore) print(" OK") print() print("=== 4. Session.to_dict() ne contient jamais le secret, seulement le mode ===") session = Session(name="prod", host="10.0.0.1", username="root", auth_type=AUTH_PASSWORD, password_storage=PASSWORD_STORAGE_SECURE) serialized = json.dumps(session.to_dict()) print(" cle presente:", "password_storage" in serialized, "| secret absent: 's3cret' not in json") assert "password_storage" in serialized assert "s3cret" not in serialized assert "password" not in session.to_dict() or True # aucun champ "password" n'existe sur Session assert not hasattr(session, "password") print(" OK") print() print("=== 5. ConnectionController : memorise puis reutilise le mot de passe ===") session_store = SessionStore(path=tmp / "sessions.json") key_manager_broadcast_widget = QWidget() broadcast = BroadcastManager(lambda: [key_manager_broadcast_widget]) from app.key_manager import KeyManager key_manager = KeyManager(registry_path=tmp / "keys.json") fake_store = FakeCredentialStore() cc = ConnectionController(session_store, key_manager, broadcast, credential_store=fake_store, parent_widget=key_manager_broadcast_widget) remembered = Session(name="r", host="10.0.0.2", username="root", auth_type=AUTH_PASSWORD, password_storage=PASSWORD_STORAGE_SECURE) session_store.add(remembered) with patch("app.controllers.connection_controller.ask_secret", return_value=("motdepasse123", True)) as asked: ok, secret = cc._ask_session_password(remembered) assert ok and secret == "motdepasse123" assert asked.called, "premiere fois : doit demander" print(" premiere connexion -> mot de passe demande puis memorise") assert fake_store.get_secret(session_credential_key(remembered.id)) == "motdepasse123" with patch("app.controllers.connection_controller.ask_secret") as asked2: ok2, secret2 = cc._ask_session_password(remembered) assert ok2 and secret2 == "motdepasse123" assert not asked2.called, "deuxieme fois : ne doit pas redemander" print(" deuxieme connexion -> mot de passe reutilise sans redemander : OK") print() print("=== 6. Suppression de session : le secret memorise est aussi supprime ===") sc = SessionController(session_store, key_manager, broadcast.button, parent_widget=key_manager_broadcast_widget, credential_store=fake_store, openssh_config_path=tmp / "no-ssh-config") with patch("app.controllers.session_controller.QMessageBox.question", return_value=QMessageBox.Yes): sc.delete_session(remembered) assert fake_store.get_secret(session_credential_key(remembered.id)) is None print(" OK") print() print("=== 7. Bastion en mode session reutilise le mot de passe deja memorise ===") bastion_session = Session(name="bastion", host="10.0.0.3", username="jump", auth_type=AUTH_PASSWORD, password_storage=PASSWORD_STORAGE_SECURE) session_store.add(bastion_session) fake_store.set_secret(session_credential_key(bastion_session.id), "bastion-secret") target = BastionTarget( host=bastion_session.host, port=22, username=bastion_session.username, auth_type=AUTH_PASSWORD, credential_key=session_credential_key(bastion_session.id), ) with patch("app.controllers.connection_controller.ask_secret") as asked3: session2 = Session(name="cible", host="10.0.0.4", username="root") with patch("app.controllers.connection_controller.resolve_bastion_target", return_value=target): ok3, resolved_target, password3 = cc._resolve_bastion(session2) assert ok3 and password3 == "bastion-secret" assert not asked3.called, "le mot de passe du bastion doit etre reutilise, pas redemande" print(" OK") no_ssh_thread.stop() print() print("CREDENTIAL VAULT : TOUS LES TESTS OK")