""" Vérifie que les callbacks CSRF n'utilisent pas d'IDs string page-spécifiques comme outputs, ce qui provoquerait des erreurs Dash "id non trouvé dans le layout". """ import dash def _find_component_id(component, target_id): """Parcourt récursivement le layout Dash à la recherche d'un composant par id.""" if hasattr(component, "id") and component.id == target_id: return True children = getattr(component, "children", None) if isinstance(children, list): return any(_find_component_id(c, target_id) for c in children) if children is not None: return _find_component_id(children, target_id) return False def test_csrf_token_store_in_main_layout(): """dcc.Store(id='csrf-token') doit être dans le layout principal (toujours présent).""" from src.app import app assert _find_component_id(app.layout, "csrf-token"), ( "dcc.Store(id='csrf-token') manquant dans le layout principal. " "Sans lui, les callbacks CSRF référencent des composants absents du layout initial." ) def test_fill_csrf_inputs_allows_initial_call(): """_fill_csrf_inputs ne doit pas avoir prevent_initial_call=True. Avec prevent_initial_call=True, le callback n'est pas déclenché lors de la chaîne initiale (_pages_location → _generate_csrf_token → csrf-token → _fill_csrf_inputs), laissant le champ csrf_token vide → erreur 400 au premier chargement de /connexion. """ from src.app import app # enregistre les callbacks et expose callback_map # Tant que l'app n'a pas été servie, les callbacks vivent dans # dash._callback.GLOBAL_CALLBACK_MAP. Dès la première requête (qu'un test # précédent a pu déclencher), Dash les déplace dans app.callback_map et vide # le registre global. On inspecte donc les deux pour ne pas dépendre de # l'ordre des tests. found = False registries = list(dash._callback.GLOBAL_CALLBACK_MAP.values()) + list( app.callback_map.values() ) for cb_info in registries: inputs = getattr(cb_info, "inputs", None) or cb_info.get("inputs", []) for inp in inputs: if hasattr(inp, "component_id"): inp_id, inp_prop = inp.component_id, inp.component_property else: inp_id, inp_prop = inp.get("id"), inp.get("property") if inp_id == "csrf-token" and inp_prop == "data": pic = getattr(cb_info, "prevent_initial_call", None) if pic is None: pic = cb_info.get("prevent_initial_call", False) assert not pic, ( "_fill_csrf_inputs a prevent_initial_call=True — le token CSRF " "ne sera pas injecté lors du premier chargement de /connexion." ) found = True assert found, ( "Callback avec Input('csrf-token', 'data') introuvable dans le registre Dash. " "Vérifier que _fill_csrf_inputs est toujours enregistré dans src/app.py." ) def test_no_page_specific_csrf_callback_outputs(): """Aucun callback CSRF ne doit cibler un ID string page-spécifique en Output.""" old_ids = { "csrf-login.value", "csrf-signup.value", "csrf-forgot.value", "csrf-change.value", "csrf-logout.value", "csrf-reset.value", "csrf-navbar-logout.value", } found = [k for k in dash._callback.GLOBAL_CALLBACK_MAP if k in old_ids] assert not found, ( "Callbacks CSRF avec IDs string trouvés — provoquent des erreurs Dash au démarrage.\n" + "\n".join(found) )