c4aa90b0d1
- Votes cappés à VOTES_PER_WEEK (pas d'accumulation) pour valoriser les connexions régulières - Solde de votes affiché dans la liste (input inéditable) à la place du bandeau Alert, avec date de prochain rechargement - Compteur de votes par feature affiché comme input inéditable en fin de ligne (avant le bouton "+") - Animation FLIP JS (roadmap_flip.js) : seules les lignes qui changent de position sont animées après un vote - Renommage votes_credited_until → votes_last_credited_at (migration 0005) - Constantes du module utilisées dans les assertions de tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
91 lines
3.6 KiB
Python
91 lines
3.6 KiB
Python
"""
|
|
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)
|
|
)
|