Retour au blog
Tests

PrestaFlow : écrire le scénario avant le fix, en faire le garde-fou

PrestaEdit •
PrestaFlow : écrire le scénario avant le fix, en faire le garde-fou

Le workflow habituel, et son défaut

Un bug remonte dans le tracker. Le workflow classique ressemble à ça :

  1. Lire le rapport, identifier ce que le user voulait faire.
  2. Ouvrir le code, chercher l’endroit fautif.
  3. Écrire le fix.
  4. Tester manuellement une fois pour vérifier — souvent en refaisant à la main les étapes décrites dans le bug.
  5. Merger.

Ça marche. Et pourtant, six mois plus tard, la même régression revient. Personne dans l’équipe ne se souvient du bug d’origine, personne n’a formalisé sa reproduction, un refactoring innocent le fait ressurgir. Le CI est vert parce qu’il ne teste pas ce cas précis — comment le pourrait-il, il n’existe pas de scénario qui le couvre.

Le renversement TDD

Le pattern TDD, appliqué à PrestaFlow, ré-ordonne :

  1. Bug reçu dans le tracker.
  2. Écrire le scénario PrestaFlow qui reproduit le bug — avant tout code de fix.
  3. Run local rouge — confirmation qu’on a bien compris le bug (si le scénario passe vert du premier coup, le bug n’est pas là où on croit).
  4. Écrire le fix.
  5. Run local vert — confirmation que le fix fait vraiment ce qu’on croit (si le scénario reste rouge, le fix ne couvre pas le cas décrit).
  6. Committer le fix ET le scénario — le scénario reste dans la suite, il pèsera quelques secondes à chaque run futur, et il rattrape la régression le jour où elle reviendra.

L’étape 3 est celle qu’on saute habituellement. Elle est cruciale — un scénario qui passe vert sans le fix indique qu’on regarde la mauvaise partie du code. Autant s’en rendre compte avant d’écrire 40 lignes de patch qui ne servent à rien.

Cas concret avec psflowdemo

Bug fictif mais représentatif : “le titre du bloc n’est pas échappé côté template. Si un admin saisit un titre contenant du HTML (<script>alert(1)</script>Bienvenue), le navigateur exécute le script au lieu d’afficher le texte.”

Côté code, le coupable tient en un mot. Le front de PrestaShop échappe par défaut les variables Smarty ; pour obtenir ce bug, il faut que le template l’ait désactivé, avec {$psflowdemo_title nofilter} dans views/templates/hook/displayHome.tpl.

Étape 1 — Écrire le scénario qui reproduit

Créez tests/prestaflow/Suites/Regression/NoXssInBlockTitle.php :

<?php

namespace Tests\Suites\Regression;

use PrestaFlow\Library\Expects\Expect;
use PrestaFlow\Library\Tests\TestsSuite;

/**
 * Regression #142 — le titre du bloc était affiché sans échappement par
 * views/templates/hook/displayHome.tpl. Ne supprimez jamais cette suite.
 */
class NoXssInBlockTitle extends TestsSuite
{
    public function init()
    {
        $this->importPage('BackOffice\Login');
        $this->importPage('Modules\Psflowdemo\Configuration', domain: 'Tests');
        $this->importPage('Modules\Psflowdemo\Home', domain: 'Tests');

        extract($this->pages);

        $payload = '<script>window.__xssFired=true</script>Bienvenue';
        $defaultTitle = 'Bienvenue sur notre boutique';

        $this
        ->describe('Regression #142 — pas de XSS via le titre du bloc')
        ->it('se connecte au BO', function () use ($backOfficeLoginPage) {
            $backOfficeLoginPage->goToPage('login');
            $backOfficeLoginPage->login();
        })
        ->it('accepte un titre contenant du HTML/JS depuis la config', function () use ($modulesPsflowdemoConfigurationPage, $payload) {
            $modulesPsflowdemoConfigurationPage->openConfiguration($_ENV['PRESTAFLOW_BO_URL']);
            $modulesPsflowdemoConfigurationPage->fillTitle($payload);
            $modulesPsflowdemoConfigurationPage->save();
        })
        ->it('n\'exécute pas le script sur la home', function () use ($modulesPsflowdemoHomePage) {
            $modulesPsflowdemoHomePage->goToPage('home');

            // '<script' et non '<script>' : PrestaShop passe la valeur dans
            // HTMLPurifier, qui l'enregistre sous la forme
            // <script type="text/javascript"><!--//--><![CDATA[ ...
            Expect::that($modulesPsflowdemoHomePage->getBlockTitle())
                ->contains('<script');

            $xssFired = $modulesPsflowdemoHomePage->getPage()
                ->evaluate('window.__xssFired === true')
                ->getReturnValue();

            Expect::that($xssFired)->isTheSameAs(false);
        })
        ->it('remet le titre par défaut', function () use ($modulesPsflowdemoConfigurationPage, $defaultTitle) {
            $modulesPsflowdemoConfigurationPage->openConfiguration($_ENV['PRESTAFLOW_BO_URL']);
            $modulesPsflowdemoConfigurationPage->fillTitle($defaultTitle);
            $modulesPsflowdemoConfigurationPage->save();
        });
    }
}

Deux détails font que ce scénario peut passer au vert une fois le bug corrigé :

  • HTMLPurifier. Le module enregistre le titre avec Configuration::updateValue(..., true) (HTML autorisé), donc PrestaShop le fait passer par HTMLPurifier. Celui-ci ne supprime pas la balise, il la réécrit en <script type="text/javascript"><!--//--><![CDATA[…. Une assertion sur <script> échouerait même avec le fix. On cherche <script.
  • Les entités HTML. Une fois le template corrigé, la page contient &lt;script…, et getTextContent() renvoie ces entités telles quelles. La méthode getBlockTitle() de la Page Home de psflowdemo les décode (html_entity_decode()) avant de rendre le titre.

Le dernier it remet le titre par défaut, pour que le run suivant reparte d’un état connu.

Étape 2 — Confirmer le rouge

composer prestaflow -- run ./tests/prestaflow/Suites/Regression

En v1.7.1, run attend un dossier, pas un fichier : on vise Suites/Regression pour la boucle courte, la CI lance tout ./tests/prestaflow. Gardez le ./ devant le chemin : cette version met la première lettre du chemin en majuscule (Tests/prestaflow), ce qui passe sur macOS mais pas sous Linux, donc pas en CI.

Le troisième it tombe : getBlockTitle() retourne "Bienvenue" seul (le <script> a été interprété par le navigateur et n’apparaît pas dans le texte visible). Le it s’arrête à cette première assertion : la seconde, sur window.__xssFired, n’est pas évaluée, mais elle aurait échoué aussi, pour la même raison de fond. Le dernier it est sauté ; la boutique garde donc le titre piégé jusqu’au prochain run vert.

Preuve documentée que vous avez bien compris le problème. La capture d’erreur (voir annexe debug) montre l’état exact de la home au moment de l’échec, attachable au ticket si besoin.

Étape 3 — Fix

Dans views/templates/hook/displayHome.tpl, remplacez {$psflowdemo_title nofilter} par {$psflowdemo_title|escape:'html':'UTF-8'}. Une ligne.

Sur une boutique Flashlight, un détail de plus : les templates Smarty compilés sont conservés, le template modifié n’est pas relu. Videz le cache Smarty avant de relancer :

docker exec ps sh -c 'rm -rf /var/www/html/var/cache/*/smarty'

(ps est le nom du conteneur PrestaShop dans l’article sur la CI ; adaptez-le au vôtre.)

Étape 4 — Confirmer le vert

composer prestaflow -- run ./tests/prestaflow/Suites/Regression

Le titre s’affiche désormais comme du texte : getBlockTitle() renvoie la balise réécrite par HTMLPurifier, qui contient bien <script, et window.__xssFired reste undefined, donc window.__xssFired === true vaut false. Le scénario passe vert.

Étape 5 — Commit

Depuis la racine du dépôt du module :

git add tests/prestaflow/Suites/Regression/NoXssInBlockTitle.php
git add views/templates/hook/displayHome.tpl
git commit -m "fix(#142): escape title in home block template + regression test"

Le scénario vit désormais avec le code. Chaque futur push le rejoue. Le jour où quelqu’un touche à ce template et remet un nofilter, le CI passe rouge sur une suite dont la description, “Regression #142 — pas de XSS via le titre du bloc”, pointe directement sur le numéro d’issue.

L’effet cumulé sur le CI

Un scénario de régression comme celui-ci prend quelques secondes par run (environ cinq, connexion au BO comprise, contre une boutique Flashlight locale). Négligeable individuellement. Au fil des mois, votre dossier Suites/Regression/ grossit d’un scénario par bug corrigé, et finit par couvrir les cas limites que votre projet a rencontrés en production.

C’est votre mémoire institutionnelle — encodée en tests exécutables, pas dans des tickets fermés que personne ne relit. Le nouveau contributeur qui casse par accident un cas oublié voit son CI rouge, et la description de la suite lui donne le contexte : “Regression #142 — pas de XSS via le titre du bloc”.

Quand ça marche vraiment bien

Les scénarios end-to-end sont excellents pour les bugs qui touchent :

  • Le rendu front — un CSS qui casse un alignement, un JS qui n’exécute pas, un template qui échappe mal
  • Un enchaînement d’écrans — un tunnel de commande qui se bloque à une étape précise
  • Un état persisté — un panier qui se vide sous certaines conditions, une config qui n’est pas sauvée

Ce sont les bugs qui exigent qu’on parcoure l’app comme un utilisateur pour être reproduits. C’est exactement le terrain de PrestaFlow.

Quand un autre outil est plus pertinent

  • Bug de logique pure (calcul, format de sortie) — PHPUnit reste plus rapide, plus focus, plus lisible.
  • Bug de performance — un scénario E2E ne mesure pas la perf, il ne fait que passer/échouer. Autres outils (ab, k6, profiling PHP).
  • Bug de sécurité complexe — un scénario TDD aide à documenter et régresser une vulnérabilité connue (comme l’XSS ci-dessus), mais ne remplace pas un audit sécurité complet.

Interaction avec les autres annexes

  • Régressions visuelles — le bug affecte le rendu ? Ajoutez un visualCheckpoint en plus du Expect::that fonctionnel. La capture rouge devient une preuve visuelle du bug.
  • Multi-versions PS — bug reproductible sur v8 mais pas v9 ? Le scénario tourne sur les deux en matrice CI. Vous voyez immédiatement le périmètre du problème et pouvez fixer conditionnellement.
  • Debug — quand votre scénario tombe rouge la première fois, la capture d’erreur montre l’état exact. C’est la meilleure documentation de bug qui existe : le lecteur du ticket voit précisément la page problématique.

Notes

Dans la Série PrestaFlow — article 12 sur 23