Skip to content

feat: allow mails to be intercepted using events #120

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Feb 26, 2025
60 changes: 41 additions & 19 deletions src/Codeception/Lib/Connector/Yii2.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,18 @@
use Symfony\Component\BrowserKit\CookieJar;
use Symfony\Component\BrowserKit\History;
use Symfony\Component\BrowserKit\Request as BrowserkitRequest;
use yii\web\Request as YiiRequest;
use Symfony\Component\BrowserKit\Response;
use Yii;
use yii\base\Component;
use yii\base\Event;
use yii\base\ExitException;
use yii\base\Security;
use yii\base\UserException;
use yii\mail\BaseMessage;
use yii\mail\BaseMailer;
use yii\mail\MailEvent;
use yii\mail\MessageInterface;
use yii\web\Application;
use yii\web\ErrorHandler;
use yii\web\IdentityInterface;
use yii\web\Request;
use yii\web\Request as YiiRequest;
use yii\web\Response as YiiResponse;
use yii\web\User;

Expand All @@ -40,7 +38,21 @@ class Yii2 extends Client
{
use Shared\PhpSuperGlobalsConverter;

const CLEAN_METHODS = [

public const array MAIL_METHODS = [
self::MAIL_CATCH,
self::MAIL_EVENT_AFTER,
self::MAIL_EVENT_BEFORE,
self::MAIL_IGNORE
];

public const string MAIL_CATCH = 'catch';
public const string MAIL_EVENT_AFTER = 'after';
public const string MAIL_EVENT_BEFORE = 'before';
public const string MAIL_IGNORE = 'ignore';


const array CLEAN_METHODS = [
self::CLEAN_RECREATE,
self::CLEAN_CLEAR,
self::CLEAN_FORCE_RECREATE,
Expand All @@ -50,51 +62,55 @@ class Yii2 extends Client
* Clean the response object by recreating it.
* This might lose behaviors / event handlers / other changes that are done in the application bootstrap phase.
*/
const CLEAN_RECREATE = 'recreate';
const string CLEAN_RECREATE = 'recreate';
/**
* Same as recreate but will not warn when behaviors / event handlers are lost.
*/
const CLEAN_FORCE_RECREATE = 'force_recreate';
const string CLEAN_FORCE_RECREATE = 'force_recreate';
/**
* Clean the response object by resetting specific properties via its' `clear()` method.
* This will keep behaviors / event handlers, but could inadvertently leave some changes intact.
* @see \yii\web\Response::clear()
*/
const CLEAN_CLEAR = 'clear';
const string CLEAN_CLEAR = 'clear';

/**
* Do not clean the response, instead the test writer will be responsible for manually resetting the response in
* between requests during one test
*/
const CLEAN_MANUAL = 'manual';
const string CLEAN_MANUAL = 'manual';


/**
* @var string application config file
*/
public $configFile;
public string $configFile;

/**
* @var self::MAIL_CATCH|self::MAIL_IGNORE|self::MAIL_EVENT_AFTER|self::MAIL_EVENT_BEFORE method for handling mails
*/
public string $mailMethod;
/**
* @var string method for cleaning the response object before each request
*/
public $responseCleanMethod;
public string $responseCleanMethod;

/**
* @var string method for cleaning the request object before each request
*/
public $requestCleanMethod;
public string $requestCleanMethod;

/**
* @var string[] List of component names that must be recreated before each request
*/
public $recreateComponents = [];
public array $recreateComponents = [];

/**
* This option is there primarily for backwards compatibility.
* It means you cannot make any modification to application state inside your app, since they will get discarded.
* @var bool whether to recreate the whole application before each request
*/
public $recreateApplication = false;
public bool $recreateApplication = false;

/**
* @var bool whether to close the session in between requests inside a single test, if recreateApplication is set to true
Expand All @@ -109,7 +125,7 @@ class Yii2 extends Client


/**
* @var list<BaseMessage>
* @var list<MessageInterface>
*/
private array $emails = [];

Expand Down Expand Up @@ -211,7 +227,7 @@ public function getInternalDomains(): array

/**
* @internal
* @return list<BaseMessage> List of sent emails
* @return list<MessageInterface> List of sent emails
*/
public function getEmails(): array
{
Expand Down Expand Up @@ -290,7 +306,13 @@ public function startApp(?\yii\log\Logger $logger = null): void
unset($config['container']);
}

$config = $this->mockMailer($config);
match ($this->mailMethod) {
self::MAIL_CATCH => $config= $this->mockMailer($config),
self::MAIL_EVENT_AFTER => $config['components']['mailer']['on ' . BaseMailer::EVENT_AFTER_SEND] = fn(MailEvent $event) => $this->emails[] = $event->message,
self::MAIL_EVENT_BEFORE => $config['components']['mailer']['on ' . BaseMailer::EVENT_BEFORE_SEND] = fn(MailEvent $event) => $this->emails[] = $event->message,
self::MAIL_IGNORE => null// Do nothing
};

$app = Yii::createObject($config);
if (!$app instanceof \yii\base\Application) {
throw new ModuleConfigException($this, "Failed to initialize Yii2 app");
Expand Down Expand Up @@ -455,7 +477,7 @@ protected function mockMailer(array $config): array

$mailerConfig = [
'class' => TestMailer::class,
'callback' => function (BaseMessage $message): void {
'callback' => function (MessageInterface $message): void {
$this->emails[] = $message;
}
];
Expand Down
3 changes: 2 additions & 1 deletion src/Codeception/Lib/Connector/Yii2/TestMailer.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@

use Closure;
use yii\mail\BaseMailer;
use yii\symfonymailer\Message;

class TestMailer extends BaseMailer
{
public $messageClass = \yii\symfonymailer\Message::class;
public $messageClass = Message::class;

public Closure $callback;

Expand Down
91 changes: 49 additions & 42 deletions src/Codeception/Module/Yii2.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,13 @@
use Symfony\Component\BrowserKit\History;
use Yii;
use yii\base\Security;
use yii\web\Application as WebApplication;
use yii\db\ActiveQueryInterface;
use yii\db\ActiveRecordInterface;
use yii\helpers\Url;
use yii\mail\BaseMessage;
use yii\mail\MessageInterface;
use yii\test\Fixture;
use yii\web\Application;
use yii\web\Application as WebApplication;
use yii\web\IdentityInterface;

/**
Expand Down Expand Up @@ -92,6 +91,11 @@
* changes will get discarded.
* * `recreateApplication` - (default: `false`) whether to recreate the whole
* application before each request
* * `mailMethod` - (default: `catch`) Method for handling email via the 'mailer'
* component. `ignore` will not do anything with mail, this means mails are not
* inspectable by the test runner, using `before` or `after` will use mailer
* events; making the mails inspectable but also allowing your default mail
* handling to work
*
* You can use this module by setting params in your `functional.suite.yml`:
*
Expand Down Expand Up @@ -175,48 +179,43 @@
*
* Maintainer: **samdark**
* Stability: **stable**
*
* @phpstan-type ClientConfig array{
* responseCleanMethod: Yii2Connector::CLEAN_CLEAR|Yii2Connector::CLEAN_MANUAL|Yii2Connector::CLEAN_RECREATE,
* requestCleanMethod: Yii2Connector::CLEAN_CLEAR|Yii2Connector::CLEAN_MANUAL|Yii2Connector::CLEAN_RECREATE,
* mailMethod: Yii2Connector::MAIL_CATCH|Yii2Connector::MAIL_IGNORE|Yii2Connector::MAIL_EVENT_AFTER|Yii2Connector::MAIL_EVENT_BEFORE,
* recreateComponents: list<string>,
* recreateApplication: bool,
* closeSessionOnRecreateApplication: bool,
* applicationClass: class-string<\yii\base\Application>|null,
* configFile: string
* }
* @phpstan-type ModuleConfig array{
* fixturesMethod: string,
* cleanup: bool,
* ignoreCollidingDSN: bool,
* transaction: bool|null,
* entryScript: string,
* entryUrl: string,
* configFile: string|null,
* responseCleanMethod: Yii2Connector::CLEAN_CLEAR|Yii2Connector::CLEAN_MANUAL|Yii2Connector::CLEAN_RECREATE,
* requestCleanMethod: Yii2Connector::CLEAN_CLEAR|Yii2Connector::CLEAN_MANUAL|Yii2Connector::CLEAN_RECREATE,
* recreateComponents: list<string>,
* recreateApplication: bool,
* closeSessionOnRecreateApplication: bool,
* applicationClass: class-string<\yii\base\Application>|null
* configFile: string|null,
* fixturesMethod: string,
* cleanup: bool,
* ignoreCollidingDSN: bool,
* transaction: bool|null,
* entryScript: string,
* entryUrl: string,
* responseCleanMethod: Yii2Connector::CLEAN_CLEAR|Yii2Connector::CLEAN_MANUAL|Yii2Connector::CLEAN_RECREATE,
* requestCleanMethod: Yii2Connector::CLEAN_CLEAR|Yii2Connector::CLEAN_MANUAL|Yii2Connector::CLEAN_RECREATE,
* mailMethod: Yii2Connector::MAIL_CATCH|Yii2Connector::MAIL_IGNORE|Yii2Connector::MAIL_EVENT_AFTER|Yii2Connector::MAIL_EVENT_BEFORE,
* recreateComponents: list<string>,
* recreateApplication: bool,
* closeSessionOnRecreateApplication: bool,
* applicationClass: class-string<\yii\base\Application>|null
* }
*
* @phpstan-type ValidConfig array{
* fixturesMethod: string,
* cleanup: bool,
* ignoreCollidingDSN: bool,
* @phpstan-type ValidConfig (ModuleConfig & array{
* transaction: bool|null,
* entryScript: string,
* entryUrl: string,
* configFile: string,
* responseCleanMethod: Yii2Connector::CLEAN_CLEAR|Yii2Connector::CLEAN_MANUAL|Yii2Connector::CLEAN_RECREATE,
* requestCleanMethod: Yii2Connector::CLEAN_CLEAR|Yii2Connector::CLEAN_MANUAL|Yii2Connector::CLEAN_RECREATE,
* recreateComponents: list<string>,
* recreateApplication: bool,
* closeSessionOnRecreateApplication: bool,
* applicationClass: class-string<\yii\base\Application>|null
* }
* @phpstan-type SessionBackup array{cookie: array<mixed>, session: array<mixed>, headers: array<string, string>, clientContext: array{ cookieJar: CookieJar, history: History }}
* @phpstan-type ClientConfig array{
* configFile: string,
* responseCleanMethod: Yii2Connector::CLEAN_CLEAR|Yii2Connector::CLEAN_MANUAL|Yii2Connector::CLEAN_RECREATE,
* requestCleanMethod: Yii2Connector::CLEAN_CLEAR|Yii2Connector::CLEAN_MANUAL|Yii2Connector::CLEAN_RECREATE,
* recreateComponents: list<string>,
* recreateApplication: bool,
* closeSessionOnRecreateApplication: bool,
* applicationClass: class-string<\yii\base\Application>|null
* }
* configFile: string
* })
* @phpstan-type SessionBackup array{
* cookie: array<mixed>,
* session: array<mixed>,
* headers: array<string, string>,
* clientContext: array{ cookieJar: CookieJar, history: History }
* }
*/
class Yii2 extends Framework implements ActiveRecord, MultiSession, PartedModule
{
Expand All @@ -241,6 +240,7 @@ class Yii2 extends Framework implements ActiveRecord, MultiSession, PartedModule
'requestCleanMethod' => Yii2Connector::CLEAN_RECREATE,
'recreateComponents' => [],
'recreateApplication' => false,
'mailMethod' => Yii2Connector::MAIL_CATCH,
'closeSessionOnRecreateApplication' => true,
'applicationClass' => null,
];
Expand Down Expand Up @@ -346,6 +346,12 @@ protected function validateConfig(): void
"The response clean method must be one of: " . $validMethods
);
}
if (!in_array($this->config['mailMethod'], Yii2Connector::MAIL_METHODS, true)) {
throw new ModuleConfigException(
self::class,
"The mail method must be one of: " . $validMethods
);
}
if (!in_array($this->config['requestCleanMethod'], Yii2Connector::CLEAN_METHODS, true)) {
throw new ModuleConfigException(
self::class,
Expand All @@ -367,6 +373,7 @@ private function configureClient(array $settings): void
$client->recreateApplication = $settings['recreateApplication'];
$client->closeSessionOnRecreateApplication = $settings['closeSessionOnRecreateApplication'];
$client->applicationClass = $settings['applicationClass'];
$client->mailMethod = $settings['mailMethod'];
$client->resetApplication();
}

Expand Down Expand Up @@ -797,7 +804,7 @@ public function dontSeeEmailIsSent(): void
* ```
*
* @part email
* @return list<BaseMessage&MessageInterface> List of sent emails
* @return list<MessageInterface> List of sent emails
* @throws \Codeception\Exception\ModuleException
*/
public function grabSentEmails(): array
Expand All @@ -820,7 +827,7 @@ public function grabSentEmails(): array
* ```
* @part email
*/
public function grabLastSentEmail(): BaseMessage|null
public function grabLastSentEmail(): MessageInterface|null
{
$this->seeEmailIsSent();
$messages = $this->grabSentEmails();
Expand Down