Retour au blog
Tests

PrestaFlow : tester les emails envoyés par un module

PrestaEdit •
PrestaFlow : tester les emails envoyés par un module

Décor

Beaucoup de modules PS déclenchent l’envoi d’un email : confirmation de commande, notification admin, alerte stock, renvoi de mot de passe, formulaire de contact. C’est un pan métier entier qui vit à côté du HTML rendu — et que les assertions front classiques de PrestaFlow ne voient pas.

Vérifier ces envois demande de capturer ce que PS envoie sans que ça finisse dans une vraie boîte mail. C’est le rôle d’un MTA de capture — MailHog, Mailpit, MailCatcher — qu’on branche à côté de la boutique de test. PrestaShop envoie ses emails vers ce MTA, PrestaFlow les récupère via son API HTTP, et vos scénarios vérifient le contenu.

Cette annexe montre le pattern de bout en bout avec Mailpit, un outil écrit en Go et inspiré de MailHog (ce n’en est pas un fork), qui expose une UI web et une API REST, et que son auteur publie en image Docker (axllent/mailpit).

Le montage : Flashlight + Mailpit + config SMTP

En CI (adapter les patterns pour local Valet/MAMP est trivial), on ajoute un side-car Mailpit à côté de Flashlight :

- name: Start MariaDB and Mailpit
  run: |
    docker network create prestashop
    docker run -d --name mysql --network prestashop \
      -e MARIADB_ROOT_PASSWORD=prestashop \
      -e MARIADB_DATABASE=prestashop \
      -e MARIADB_USER=prestashop \
      -e MARIADB_PASSWORD=prestashop \
      mariadb:11
    docker run -d --name mailpit --network prestashop \
      -p 1025:1025 \
      -p 8025:8025 \
      axllent/mailpit:latest

- name: Start PrestaShop (Flashlight)
  run: |
    docker run -d --name ps --network prestashop \
      -p 80:80 \
      -e PS_DOMAIN=localhost \
      -e MYSQL_HOST=mysql \
      -v ${{ github.workspace }}/tests/flashlight-init:/tmp/init-scripts:ro \
      prestashop/prestashop-flashlight:8.1.7

Les trois conteneurs partagent un réseau Docker : PrestaShop joint la base sous le nom mysql et le SMTP sous le nom mailpit. Flashlight n’embarque pas MySQL, d’où la base à côté, et PS_DOMAIN est obligatoire. Pour l’attente du boot, reprenez la boucle sur /admin-dev/ de l’article sur la CI.

Deux ports Mailpit exposés :

  • 1025 : port SMTP standard, celui vers lequel PrestaShop enverra
  • 8025 : UI web + API REST — c’est celle qu’on consomme depuis PrestaFlow

Il faut ensuite dire à PrestaShop d’utiliser ce SMTP. Le plus propre : un init-script Flashlight (cf. annexe fixtures) qui configure les paramètres SMTP au boot :

tests/flashlight-init/30-configure-smtp.sh

#!/usr/bin/env bash
set -e

cd /var/www/html

php bin/console prestashop:config set PS_MAIL_METHOD --value 2
php bin/console prestashop:config set PS_MAIL_SERVER --value mailpit
php bin/console prestashop:config set PS_MAIL_SMTP_PORT --value 1025
php bin/console prestashop:config set PS_MAIL_SMTP_ENCRYPTION --value off
php bin/console prestashop:config set PS_MAIL_USER --value ""
php bin/console prestashop:config set PS_MAIL_PASSWD --value ""

Chaque email que PS envoie part vers mailpit:1025 et se retrouve consultable via http://localhost:8025/api/v1/messages.

Une Page qui interroge l’API Mailpit

Créez tests/prestaflow/Pages/v8/Support/Mailbox/Page.php — une Page utilitaire (pas liée à une URL du site) qui expose les vérifications sur les emails capturés :

<?php

namespace Tests\Pages\v8\Support\Mailbox;

use PrestaFlow\Library\Pages\v8\FrontOffice\Page as BasePage;

class Page extends BasePage
{
    private string $mailpitUrl = 'http://localhost:8025';

    public function reset(): void
    {
        // DELETE sans liste d'IDs : Mailpit vide toute la boîte.
        $this->httpDelete($this->mailpitUrl . '/api/v1/messages');
    }

    public function listMessages(): array
    {
        $body = $this->httpGet($this->mailpitUrl . '/api/v1/messages');
        return json_decode($body, true)['messages'] ?? [];
    }

    public function findByRecipient(string $email): ?array
    {
        foreach ($this->listMessages() as $m) {
            foreach ($m['To'] ?? [] as $to) {
                if (($to['Address'] ?? '') === $email) {
                    return $m;
                }
            }
        }
        return null;
    }

    public function waitForMessage(string $email, int $timeout = 10000): array
    {
        $found = null;
        $this->waitUntil(function () use ($email, &$found) {
            $found = $this->findByRecipient($email);
            return $found !== null;
        }, $timeout, 'Aucun email reçu pour ' . $email);

        return $found;
    }

    public function fetchMessage(string $messageId): array
    {
        $body = $this->httpGet($this->mailpitUrl . '/api/v1/message/' . $messageId);
        return json_decode($body, true) ?? [];
    }

    public function getMessageBody(string $messageId): string
    {
        // Version texte de l'email (champ Text de l'API), sans l'enveloppe JSON.
        return (string) ($this->fetchMessage($messageId)['Text'] ?? '');
    }

    private function httpGet(string $url): string
    {
        return (string) file_get_contents($url);
    }

    private function httpDelete(string $url): void
    {
        $ctx = stream_context_create(['http' => ['method' => 'DELETE', 'ignore_errors' => true]]);
        @file_get_contents($url, false, $ctx);
    }
}

Volontairement simple — pas d’HTTP client tiers, file_get_contents suffit pour l’API JSON de Mailpit.

Un scénario : vérifier l’email envoyé par le formulaire contact

Objectif : simuler un client qui envoie une demande via le formulaire de contact ps_contactform. Vérifier que l’email atterrit bien chez l’admin.

Deux précisions avant le code. La boutique Flashlight n’existe qu’en anglais : le formulaire est sur /contact-us, que goToPage('contact-us') construit à partir de PRESTAFLOW_FO_URL. Et $adminEmail doit être l’adresse du contact sélectionné par défaut dans le formulaire (BO → Paramètres de la boutique → Contact) : vérifiez-la une fois sur votre boutique de test.

<?php

namespace Tests\Suites;

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

class ContactFormEmail extends TestsSuite
{
    public function init()
    {
        $this->importPage('FrontOffice\Contact');
        $this->importPage('Support\Mailbox', domain: 'Tests');

        extract($this->pages);

        $customerMessage = 'Test PrestaFlow #' . time();
        $adminEmail = 'admin@prestashop.com';

        $this
        ->describe('Le formulaire contact envoie un email')
        ->it('nettoie la boîte capturée', function () use ($supportMailboxPage) {
            $supportMailboxPage->reset();
            Expect::that(count($supportMailboxPage->listMessages()))->isTheSameAs(0);
        })
        ->it('soumet le formulaire contact', function () use ($frontOfficeContactPage, $customerMessage) {
            // Flashlight n'installe que l'anglais : la page s'appelle contact-us.
            $frontOfficeContactPage->goToPage('contact-us');
            $frontOfficeContactPage->setValue('input[name="from"]', 'client@example.test');
            $frontOfficeContactPage->setValue('textarea[name="message"]', $customerMessage);
            $frontOfficeContactPage->clickAndWaitReload('input[name="submitMessage"]');
            $frontOfficeContactPage->waitForText('Your message has been successfully sent to our team.');
        })
        ->it('capture un email chez l\'admin', function () use ($supportMailboxPage, $adminEmail) {
            $message = $supportMailboxPage->waitForMessage($adminEmail);
            Expect::that($message)->isNotNull();
        })
        ->it('l\'email contient le message client', function () use ($supportMailboxPage, $adminEmail, $customerMessage) {
            $message = $supportMailboxPage->findByRecipient($adminEmail);
            $body = $supportMailboxPage->getMessageBody($message['ID']);

            Expect::that($body)->contains($customerMessage);
            Expect::that($body)->contains('client@example.test');
        });
    }
}

Quatre it, qui répondent aux quatre questions qu’on se pose sur un envoi d’email :

  • La boîte de départ est bien vide (pas de pollution d’un précédent test)
  • L’action UI déclenche bien un envoi SMTP
  • L’email arrive au bon destinataire
  • Le contenu de l’email est celui attendu

Vérifier les pièces jointes

Mailpit décrit les pièces jointes d’un message dans le champ Attachments de GET /api/v1/message/{ID} (nom, type, taille, PartID), et sert chacune via GET /api/v1/message/{ID}/part/{PartID}. Utile quand un module génère un PDF de facture attaché à l’email de confirmation.

public function getAttachments(string $messageId): array
{
    return $this->fetchMessage($messageId)['Attachments'] ?? [];
}

Un it qui vérifie la présence d’un PDF :

->it('l\'email contient une facture PDF', function () use ($supportMailboxPage) {
    $message = $supportMailboxPage->waitForMessage('client@example.test');
    $attachments = $supportMailboxPage->getAttachments($message['ID']);

    $pdf = array_filter($attachments, fn($a) => str_ends_with($a['FileName'] ?? '', '.pdf'));

    Expect::that(count($pdf))->isTheSameAs(1);
});

Localement, sans Docker

Si vous testez sur la stack locale (Valet, MAMP), Mailpit tourne aussi en binaire natif : brew install mailpit sur macOS, puis mailpit lance le SMTP sur 1025 et l’UI sur 8025. Config PS pareille (PS_MAIL_SERVER=127.0.0.1, PS_MAIL_SMTP_PORT=1025), la Page Support\Mailbox fonctionne à l’identique.

Interaction avec les autres annexes

  • Fixtures — la config SMTP fait partie de l’état initial de la boutique, donc niveau 1 (init-script Flashlight) est le bon endroit pour la poser. Cette annexe applique exactement le pattern.
  • Auth cookies — si votre scénario email nécessite un login client (email de suivi de commande), le cookie pré-injecté fait sauter l’étape login. Combiné : boutique SMTP-configurée + session client déjà ouverte + captation Mailpit = suite d’email test en 3 it.
  • TDD — bug remonté “le mail admin n’est plus envoyé après update du module” → scénario de reproduction (soumet formulaire + attend email) → rouge → fix → vert. Le scénario reste comme garde-fou.

Notes

Dans la Série PrestaFlow — article 26 sur 35