08.08 — BFCache Chrome (B-BROWSER-1) + artefacts d’environnement#

Chrome restore une page no-store après logout, depuis le BFCache, alors qu’il s’agit d’une page protégée par authentification.

B-BROWSER-1 — Chrome nous emmerde avec son BFCache#

Symptôme#

Après déconnexion, cliquer sur le bouton “précédent” afin d’être redirigé vers une page qui était protégée par authentification (par exemple : history.php) :

  • Chrome : la page reste affichée avec son contenu tel qu’il est affiché à un utilisateur encore authentifié. L’URL reste history.php, le serveur n’est pas recontacté pour valider ou invalider l’accès.
  • Firefox : une nouvelle requête est envoyée au serveur, ce dernier voit que la session est morte, il redirige.

Gap très ennuyeux#

history.php envoie correctement :

  • Cache-Control: no-store, no-cache, must-revalidate
  • Pragma: no-cache
  • Expires

Et l’invalidation server-side de la session est correcte.

Mais le BFCache de Chrome restaure quand même la page depuis un snapshot local.

Recette de confirmation : back-then-reload#

back() → on est sur history.php, page authentifiée restaurée
   ↓ (rien n'a touché le serveur)
refresh() → reload force un round-trip serveur
   ↓
serveur : session morte → 302 vers homepage
   ↓
on est redirigé
  • back() nous affiche à nouveau la page que l’on ne devrait plus voir ET refresh() provoque notre redirection → BFCache hit confirmé.
  • back() redirige immédiatement → invalidation server-side qui fonctionne comme attendu.
  • back() nous affiche à nouveau la page que l’on ne devrait plus voir ET refresh() ne nous redirige pas → l’invalidation server-side est cassée (anomalie).

CLAUDE.md :

A confirmed BFCache exposure raises a dedicated, non-transient exception — BackForwardCacheExposureError (src/lib/errors.py). Never a bare AssertionError; never a Selenium WebDriverException. The finding is deterministic and must never land in transient_errors.

Exception dédiée#

# src/lib/errors.py
class BackForwardCacheExposureError(Exception):
    """The browser's back-forward cache exposed a logged-out authenticated view."""

Ce n’est PAS une exception de la collection transient_errors. Cette trouvaille est déterministe : on constate immédiatement le problème, ce n’est pas à considérer comme étant un flake.

Tests associés#

TestRecipe
post_logout_bfcache_exposure.pyback-then-reload : si back squatte une page qui ne devrait plus être affichée + refresh redirige → BFCacheExposureError
post_logout_server_invalidation.pyrefresh direct : confirme que le serveur invalide bien (PASS)
post_logout_frenetic_navigation.pyback/forward stress (3 cycles) : tente de provoquer au max un timing mystérieux avec le BFCache
  • post_logout_bfcache_exposure : ÉCHEC sur Chrome, SUCCÈS sur Firefox.
  • post_logout_server_invalidation : SUCCÈS sur les deux (le serveur fait sa part).

Code#

class HistoryPage(SeleniumBackAndForwardNavigationMixin, SeleniumTitleMixin, POMBase):
    def verify_back_button_did_not_restore_view(self) -> Self:
        # On est sur history.php après back()
        pre_url = self._driver.current_url
        self._driver.refresh()
        try:
            WebDriverWait(self._driver, get_timeout()).until(
                ec.url_changes(pre_url)
            )
            # ✅ refresh a redirigé → server-side invalidation marche
            # → back() stayed put = BFCache hit
            raise BackForwardCacheExposureError(
                f"Browser BFCache restored authenticated view of {pre_url} after logout."
            )
        except TimeoutException:
            # refresh n'a pas redirigé → BFCache OR server-side fail
            # ... distinguer ...
            raise

→ La logique de distinction back→refresh est encapsulée dans le POM. Le scénario appelle juste verify_back_button_did_not_restore_view.

« Cross-browser difference is a finding »#

CLAUDE.md :

Never skip a test on the browser where it fails.

Ce test reste actif sur les deux navigateurs.

  1. Sur Chrome, il documente déterministiquement la fragilité.
  2. Sur Firefox, il documente que Firefox honore no-store.

Recommandation à CURA#

IDENTIFIED_GAPS.md :

Recommendation for CURA: send Clear-Site-Data: "cache", "cookies" on the logout response so the snapshot is evicted regardless of the browser’s back-forward-cache policy.

→ Header HTTP qui, normalement, force le navigateur à détruire le snapshot. Garantit le bon comportement même avec BFCache aggressif.

A-ENV-1 — Rapid POST drop sous --workers 3#

Symptôme#

Sous --workers 3, un POST rapide et consécutif d’un des trois workers parallèles fail intermittemment.
Observé sur :

  • Appointments - Saturation (5 bookings successifs).
  • Journey - History ordered ... (2 bookings successifs).

Cause#

“Heroku eco-dyno concurrency limit hit”. C’est lié à l’infra sur laquelle est déployée le SUT. En vérifiant en single-worker, 2 réservations consécutives passent en 200-600ms.

Gestion de la flakiness#

TimeoutException ⊆ WebDriverException → ça part dans transient_errors → auto-retry.
Un fail survivant aux retries indique que l’on a potentiellement atteint un niveau de contention trop important, ce ne serait pas forcément une régression et mériterait une analyse complémentaire côté DevOps.

A-ENV-2 — Modale de détection d’un mot de passe faible (Chrome)#

Symptôme#

Sur Chrome uniquement, après un login réussi : toute action subséquente (clic, frappe clavier, touche entrée) ne faisait rien. Après analyse, on s’est rendu compte que c’était dû à une modale native dans Chrome : « your password has leaked » apparaissait et plantait les pas de test.

Cause#

ThisIsNotAPassword est dans la publicly-leaked credentials database de Google. Le gestionnaire de mots de passe de Chrome le détecte et hurle juste après la moindre connexion à l’application. C’est typiquement un problème d’environnement de test, et qui plus est qui concerne le testeur directement.

Résolution#

src/lib/ext/ocarina/adapters/selenium/create_drivers_pool.py :

options.add_experimental_option("prefs", {
    "credentials_enable_service": False,
    "profile.password_manager_enabled": False,
    "profile.password_manager_leak_detection": False,
})
options.add_argument("--disable-features=PasswordLeakDetection")

→ Désactive le gestionnaire de mots de passe de Chrome + la fonctionnalité de “leak detection”.
La modale non pertinente n’apparaît plus.

IDENTIFIED_GAPS.md#

Why it stays documented: the entry preserves why the chrome driver adapter exists — delete the adapter and this artifact returns.

→ Mémoire. Si un refactor futur supprime l’adapter custom, on saura pourquoi il avait été ajouté.

Conventions sémantiques#

PréfixeCatégorieExemple
G-Gap (CURA est défaillant)G-SEC-1, G-DATA-1, G-SPEC-1
B-Browser (comportement du navigateur)B-BROWSER-1
A-Artefact d’environnementA-ENV-1 (dyno contention), A-ENV-2 (Chrome modal)