Browse Source

Added EmailTrait and TestEmailTransport for making assertions on emails

Jeremy Harris 8 years ago
parent
commit
e5d99f6f3a

+ 69 - 0
src/TestSuite/Constraint/MailConstraintBase.php

@@ -0,0 +1,69 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ * @link          https://cakephp.org CakePHP(tm) Project
+ * @since         3.7.0
+ * @license       https://opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\TestSuite\Constraint;
+
+use Cake\TestSuite\TestEmailTransport;
+use PHPUnit\Framework\Constraint\Constraint;
+
+/**
+ * Base class for all mail assertion constraints
+ */
+class MailConstraintBase extends Constraint
+{
+
+    protected $at;
+
+    /**
+     * Constructor
+     *
+     * @param int $at At
+     * @return void
+     */
+    public function __construct($at = null)
+    {
+        $this->at = $at;
+        parent::__construct();
+    }
+
+    /**
+     * Gets the email or emails to check
+     *
+     * @return Cake\Mailer\Email|array
+     */
+    public function getEmails()
+    {
+        $emails = TestEmailTransport::getEmails();
+
+        if ($this->at) {
+            if (!isset($emails[$this->at])) {
+                return [];
+            }
+
+            return [$emails[$this->at]];
+        }
+
+        return $emails;
+    }
+
+    /**
+     * noop needed for abstract
+     *
+     * @return string
+     */
+    public function toString()
+    {
+        return 'base constraint, do not use';
+    }
+}

+ 52 - 0
src/TestSuite/Constraint/MailContainsConstraint.php

@@ -0,0 +1,52 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ * @link          https://cakephp.org CakePHP(tm) Project
+ * @since         3.7.0
+ * @license       https://opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\TestSuite\Constraint;
+
+/**
+ * MailContainsConstraint
+ */
+class MailContainsConstraint extends MailConstraintBase
+{
+
+    /**
+     * Checks constraint
+     *
+     * @param mixed $other Constraint check
+     * @return bool
+     */
+    public function matches($other)
+    {
+        $emails = $this->getEmails();
+        foreach ($emails as $email) {
+            $message = implode("\r\n", (array)$email->message());
+
+            return preg_match("/$other/", $message) !== false;
+        }
+        return false;
+    }
+
+    /**
+     * Assertion message string
+     *
+     * @return string
+     */
+    public function toString()
+    {
+        if ($this->at) {
+            return sprintf('is in email #%d', $this->at);
+        }
+        return 'is in an email';
+    }
+}

+ 43 - 0
src/TestSuite/Constraint/MailCountConstraint.php

@@ -0,0 +1,43 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ * @link          https://cakephp.org CakePHP(tm) Project
+ * @since         3.7.0
+ * @license       https://opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\TestSuite\Constraint;
+
+/**
+ * MailCountConstraint
+ */
+class MailCountConstraint extends MailConstraintBase
+{
+
+    /**
+     * Checks constraint
+     *
+     * @param mixed $other Constraint check
+     * @return bool
+     */
+    public function matches($other)
+    {
+        return count($this->getEmails()) === $other;
+    }
+
+    /**
+     * Assertion message string
+     *
+     * @return string
+     */
+    public function toString()
+    {
+        return 'emails were sent';
+    }
+}

+ 36 - 0
src/TestSuite/Constraint/MailSentFromConstraint.php

@@ -0,0 +1,36 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ * @link          https://cakephp.org CakePHP(tm) Project
+ * @since         3.7.0
+ * @license       https://opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\TestSuite\Constraint;
+
+/**
+ * MailSentFromConstraint
+ */
+class MailSentFromConstraint extends MailSentWithConstraint
+{
+    protected $method = 'from';
+
+    /**
+     * Assertion message string
+     *
+     * @return string
+     */
+    public function toString()
+    {
+        if ($this->at) {
+            return sprintf('sent email #%d', $this->at);
+        }
+        return 'sent an email';
+    }
+}

+ 36 - 0
src/TestSuite/Constraint/MailSentToConstraint.php

@@ -0,0 +1,36 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ * @link          https://cakephp.org CakePHP(tm) Project
+ * @since         3.7.0
+ * @license       https://opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\TestSuite\Constraint;
+
+/**
+ * MailSentToConstraint
+ */
+class MailSentToConstraint extends MailSentWithConstraint
+{
+    protected $method = 'to';
+
+    /**
+     * Assertion message string
+     *
+     * @return string
+     */
+    public function toString()
+    {
+        if ($this->at) {
+            return sprintf('was sent email #%d', $this->at);
+        }
+        return 'was sent an email';
+    }
+}

+ 73 - 0
src/TestSuite/Constraint/MailSentWithConstraint.php

@@ -0,0 +1,73 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ * @link          https://cakephp.org CakePHP(tm) Project
+ * @since         3.7.0
+ * @license       https://opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\TestSuite\Constraint;
+
+/**
+ * MailSentWithConstraint
+ */
+class MailSentWithConstraint extends MailConstraintBase
+{
+    protected $method;
+
+    /**
+     * Constructor
+     *
+     * @param string $method Method
+     * @param int $at At
+     * @return void
+     */
+    public function __construct($at = null, $method = null)
+    {
+        if ($method) {
+            $this->method = $method;
+        }
+        parent::__construct($at);
+    }
+
+    /**
+     * Checks constraint
+     *
+     * @param mixed $other Constraint check
+     * @return bool
+     */
+    public function matches($other)
+    {
+        $emails = $this->getEmails();
+        foreach ($emails as $email) {
+            $value = $email->{'get' . ucfirst($this->method)}();
+            if (in_array($this->method, ['to', 'cc', 'bcc', 'from'])) {
+                $value = key($value);
+            }
+            if ($value === $other) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Assertion message string
+     *
+     * @return string
+     */
+    public function toString()
+    {
+        if ($this->at) {
+            return sprintf('is in email #%d `%s`', $this->at, $this->method);
+        }
+        return sprintf('is in an email `%s`', $this->method);
+    }
+
+}

+ 54 - 0
src/TestSuite/Constraint/NoMailSentConstraint.php

@@ -0,0 +1,54 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ * @link          https://cakephp.org CakePHP(tm) Project
+ * @since         3.7.0
+ * @license       https://opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\TestSuite\Constraint;
+
+/**
+ * NoMailSentConstraint
+ */
+class NoMailSentConstraint extends MailConstraintBase
+{
+
+    /**
+     * Checks constraint
+     *
+     * @param mixed $other Constraint check
+     * @return bool
+     */
+    public function matches($other)
+    {
+        return count($this->getEmails()) === 0;
+    }
+
+    /**
+     * Assertion message string
+     *
+     * @return string
+     */
+    public function toString()
+    {
+        return 'no emails were sent';
+    }
+
+    /**
+     * Overwrites the descriptions so we can remove the automatic "expected" message
+     *
+     * @param mixed $other Value
+     * @return string
+     */
+    protected function failureDescription($other)
+    {
+        return $this->toString();
+    }
+}

+ 166 - 0
src/TestSuite/EmailTrait.php

@@ -0,0 +1,166 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ * @link          https://cakephp.org CakePHP(tm) Project
+ * @since         3.7.0
+ * @license       https://opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\TestSuite;
+
+use Cake\TestSuite\Constraint\MailContainsConstraint;
+use Cake\TestSuite\Constraint\MailCountConstraint;
+use Cake\TestSuite\Constraint\MailSentFromConstraint;
+use Cake\TestSuite\Constraint\MailSentToConstraint;
+use Cake\TestSuite\Constraint\MailSentWithConstraint;
+use Cake\TestSuite\Constraint\NoMailSentConstraint;
+
+/**
+ * Make assertions on emails sent through the Cake\TestSuite\TestEmailTransport
+ *
+ * **tests/bootstrap.php**
+ * ```
+ * use Cake\Mailer\Email;
+ * use Cake\TestSuite\TestEmailTransport;
+ *
+ * // replace with other transport configs if required
+ * $config = Email::getConfigTransport('default');
+ * $config['className'] = TestEmailTransport::class;
+ * Email::dropTransport('default');
+ * Email::setConfigTransport('default', $config);
+ * ```
+ */
+trait EmailTrait
+{
+
+    /**
+     * Asserts an expected number of emails were sent
+     *
+     * @param int $count Email count
+     * @param string $message Message
+     * @return void
+     */
+    public function assertMailCount($count, $message = null)
+    {
+        $this->assertThat($count, new MailCountConstraint(), $message);
+    }
+    /**
+     *
+     * Asserts that no emails were sent
+     *
+     * @param string $message Message
+     * @return void
+     */
+    public function assertNoMailSent($message = null)
+    {
+        $this->assertThat(null, new NoMailSentConstraint(), $message);
+    }
+
+    /**
+     * Asserts an email at a specific index was sent to an address
+     *
+     * @param int $at Email index
+     * @param int $address Email address
+     * @param string $message Message
+     * @return void
+     */
+    public function assertMailSentToAt($at, $address, $message = null)
+    {
+        $this->assertThat($address, new MailSentToConstraint($at), $message);
+    }
+
+    /**
+     * Asserts an email at a specific index was sent from an address
+     *
+     * @param int $at Email index
+     * @param int $address Email address
+     * @param string $message Message
+     * @return void
+     */
+    public function assertMailSentFromAt($at, $address, $message = null)
+    {
+        $this->assertThat($address, new MailSentFromConstraint($at), $message);
+    }
+
+    /**
+     * Asserts an email at a specific index contains expected contents
+     *
+     * @param int $at Email index
+     * @param int $contents Contents
+     * @param string $message Message
+     * @return void
+     */
+    public function assertMailContainsAt($at, $contents, $message = null)
+    {
+        $this->assertThat($contents, new MailContainsConstraint($at), $message);
+    }
+
+    /**
+     * Asserts an email at a specific index contains the expected value within an Email getter
+     *
+     * @param int $at Email index
+     * @param int $expected Contents
+     * @param int $parameter Email getter parameter (e.g. "cc", "subject")
+     * @param string $message Message
+     * @return void
+     */
+    public function assertMailSentWithAt($at, $expected, $parameter, $message = null)
+    {
+        $this->assertThat($expected, new MailSentWithConstraint($at, $parameter), $message);
+    }
+
+    /**
+     * Asserts an email was sent to an address
+     *
+     * @param int $address Email address
+     * @param string $message Message
+     * @return void
+     */
+    public function assertMailSentTo($address, $message = null)
+    {
+        $this->assertThat($address, new MailSentToConstraint(), $message);
+    }
+
+    /**
+     * Asserts an email was sent from an address
+     *
+     * @param int $address Email address
+     * @param string $message Message
+     * @return void
+     */
+    public function assertMailSentFrom($address, $message = null)
+    {
+        $this->assertThat($address, new MailSentFromConstraint(), $message);
+    }
+
+    /**
+     * Asserts an email contains expected contents
+     *
+     * @param int $contents Contents
+     * @param string $message Message
+     * @return void
+     */
+    public function assertMailContains($contents, $message = null)
+    {
+        $this->assertThat($contents, new MailContainsConstraint(), $message);
+    }
+
+    /**
+     * Asserts an email contains the expected value within an Email getter
+     *
+     * @param int $expected Contents
+     * @param int $parameter Email getter parameter (e.g. "cc", "subject")
+     * @param string $message Message
+     * @return void
+     */
+    public function assertMailSentWith($expected, $parameter, $message = null)
+    {
+        $this->assertThat($expected, new MailSentWithConstraint(null, $parameter), $message);
+    }
+}

+ 2 - 0
src/TestSuite/TestCase.php

@@ -33,6 +33,7 @@ use PHPUnit\Framework\TestCase as BaseTestCase;
 abstract class TestCase extends BaseTestCase
 {
 
+    use EmailTrait;
     use LocatorAwareTrait;
 
     /**
@@ -159,6 +160,7 @@ abstract class TestCase extends BaseTestCase
             Configure::write($this->_configure);
         }
         $this->getTableLocator()->clear();
+        TestEmailTransport::clearEmails();
     }
 
     /**

+ 63 - 0
src/TestSuite/TestEmailTransport.php

@@ -0,0 +1,63 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ * @link          https://cakephp.org CakePHP(tm) Project
+ * @since         3.7.0
+ * @license       https://opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\TestSuite;
+
+use Cake\Mailer\Email;
+use Cake\Mailer\AbstractTransport;
+
+/**
+ * TestEmailTransport
+ *
+ * Set this as the email transport to capture emails for later assertions
+ *
+ * @see Cake\TestSuite\EmailTrait
+ */
+class TestEmailTransport extends AbstractTransport
+{
+    private static $emails = [];
+
+    /**
+     * Stores email for later assertions
+     *
+     * @param Email $email
+     * @return bool
+     */
+    public function send(Email $email)
+    {
+        static::$emails[] = $email;
+
+        return true;
+    }
+
+    /**
+     * Gets emails sent
+     *
+     * @return array
+     */
+    public static function getEmails()
+    {
+        return static::$emails;
+    }
+
+    /**
+     * Clears list of emails that have been sent
+     *
+     * @return void
+     */
+    public static function clearEmails()
+    {
+        static::$emails = [];
+    }
+}

+ 181 - 0
tests/TestCase/TestSuite/EmailTraitTest.php

@@ -0,0 +1,181 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
+ * @link          https://cakephp.org CakePHP(tm) Project
+ * @since         3.7.0
+ * @license       https://opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Test\TestCase\TestSuite;
+
+use Cake\Mailer\Email;
+use Cake\TestSuite\TestEmailTransport;
+use Cake\TestSuite\TestCase;
+use PHPUnit\Framework\AssertionFailedError;
+
+/**
+ * Tests EmailTrait assertions
+ */
+class EmailTraitTest extends TestCase
+{
+
+    /**
+     * setUp
+     *
+     * @return void
+     */
+    public function setUp()
+    {
+        parent::setUp();
+
+        Email::setConfig('default', [
+            'transport' => 'test_tools',
+            'from' => 'default@example.com',
+        ]);
+        Email::setConfig('alternate', [
+            'transport' => 'test_tools',
+            'from' => 'alternate@example.com',
+        ]);
+        Email::setConfigTransport('test_tools', [
+            'className' => TestEmailTransport::class
+        ]);
+    }
+
+    /**
+     * tearDown
+     *
+     * @return void
+     */
+    public function tearDown()
+    {
+        parent::tearDown();
+
+        Email::drop('default');
+        Email::drop('alternate');
+        Email::dropTransport('test_tools');
+    }
+
+    /**
+     * tests assertions against any emails that were sent
+     *
+     * @return void
+     */
+    public function testSingleAssertions()
+    {
+        $this->sendEmails();
+
+        $this->assertMailSentFrom('default@example.com');
+        $this->assertMailSentFrom('alternate@example.com');
+
+        $this->assertMailSentTo('to@example.com');
+        $this->assertMailSentTo('to2@example.com');
+
+        $this->assertMailContains('message');
+
+        $this->assertMailSentWith('Hello world', 'subject');
+        $this->assertMailSentWith('cc@example.com', 'cc');
+        $this->assertMailSentWith('bcc@example.com', 'bcc');
+        $this->assertMailSentWith('cc2@example.com', 'cc');
+    }
+
+    /**
+     * tests multiple email assertions
+     *
+     * @return void
+     */
+    public function testMultipleAssertions()
+    {
+        $this->assertNoMailSent();
+
+        $this->sendEmails();
+
+        $this->assertMailCount(2);
+
+        $this->assertMailSentFromAt(0, 'default@example.com');
+        $this->assertMailSentFromAt(1, 'alternate@example.com');
+
+        $this->assertMailSentToAt(0, 'to@example.com');
+        $this->assertMailSentToAt(1, 'to2@example.com');
+
+        $this->assertMailContainsAt(0, 'message');
+        $this->assertMailContainsAt(1, 'message 2');
+
+        $this->assertMailSentWithAt(0, 'Hello world', 'subject');
+    }
+
+    /**
+     * tests assertNoMailSent fails when no mail is sent
+     *
+     * @return void
+     */
+    public function testAssertNoMailSentFailure()
+    {
+        $this->expectException(AssertionFailedError::class);
+        $this->expectExceptionMessage('Failed asserting that no emails were sent.');
+
+        $this->sendEmails();
+        $this->assertNoMailSent();
+    }
+
+    /**
+     * tests constraint failure messages
+     *
+     * @param string $assertion Assertion method
+     * @param string $expectedMessage Expected failure message
+     * @param array $params Assertion params
+     * @dataProvider failureMessageDataProvider
+     */
+    public function testFailureMessages($assertion, $expectedMessage, $params)
+    {
+        $this->expectException(AssertionFailedError::class);
+        $this->expectExceptionMessage($expectedMessage);
+
+        call_user_func_array([$this, $assertion], $params);
+    }
+
+    /**
+     * data provider for checking failure messages
+     *
+     * @return array
+     */
+    public function failureMessageDataProvider()
+    {
+        return [
+            'assertMailCount' => ['assertMailCount', 'Failed asserting that 2 emails were sent.', [2]],
+            'assertMailSentTo' => ['assertMailSentTo', 'Failed asserting that \'missing@example.com\' was sent an email.', ['missing@example.com']],
+            'assertMailSentToAt' => ['assertMailSentToAt', 'Failed asserting that \'missing@example.com\' was sent email #1.', [1, 'missing@example.com']],
+            'assertMailSentFrom' => ['assertMailSentFrom', 'Failed asserting that \'missing@example.com\' sent an email.', ['missing@example.com']],
+            'assertMailSentFromAt' => ['assertMailSentFromAt', 'Failed asserting that \'missing@example.com\' sent email #1.', [1, 'missing@example.com']],
+            'assertMailSentWith' => ['assertMailSentWith', 'Failed asserting that \'Missing\' is in an email `subject`.', ['Missing', 'subject']],
+            'assertMailSentWithAt' => ['assertMailSentWithAt', 'Failed asserting that \'Missing\' is in email #1 `subject`.', [1, 'Missing', 'subject']],
+            'assertMailContains' => ['assertMailContains', 'Failed asserting that \'Missing\' is in an email.', ['Missing']],
+            'assertMailContainsAt' => ['assertMailContainsAt', 'Failed asserting that \'Missing\' is in email #1.', [1, 'Missing']],
+        ];
+    }
+
+    /**
+     * sends some emails
+     *
+     * @return void
+     */
+    private function sendEmails()
+    {
+        (new Email())
+            ->setTo(['to@example.com' => 'Foo Bar'])
+            ->setCc('cc@example.com')
+            ->setBcc(['bcc@example.com' => 'Baz Qux'])
+            ->setSubject('Hello world')
+            ->send('message');
+
+        (new Email('alternate'))
+            ->setTo('to2@example.com')
+            ->setCc('cc2@example.com')
+            ->send('message 2');
+    }
+}