diff --git a/src/Email/EmailSender.php b/src/Email/EmailSender.php new file mode 100644 index 000000000..9b08f4121 --- /dev/null +++ b/src/Email/EmailSender.php @@ -0,0 +1,99 @@ +getMailer( + 'CakeDC/Users.Users', + $this->_getEmailInstance($email) + ) + ->send('validation', [$user, __d('Users', 'Your account validation link')]); + } + + /** + * Send the reset password email + * + * @param EntityInterface $user User entity + * @param Email $email instance, if null the default email configuration with the + * @param string $template email template + * Users.validation template will be used, so set a ->template() if you pass an Email + * instance + * @return array email send result + */ + public function sendResetPasswordEmail(EntityInterface $user, Email $email = null, $template = 'CakeDC/Users.reset_password') + { + $this + ->getMailer( + 'CakeDC/Users.Users', + $this->_getEmailInstance($email) + ) + ->send('resetPassword', [$user, $template]); + } + + /** + * Send social validation email to the user + * + * @param EntityInterface $socialAccount social account + * @param EntityInterface $user user + * @param Email $email Email instance or null to use 'default' configuration + * @return mixed + */ + public function sendSocialValidationEmail(EntityInterface $socialAccount, EntityInterface $user, Email $email = null) + { + if (empty($email)) { + $template = 'CakeDC/Users.social_account_validation'; + } else { + $template = $email->template()['template']; + } + $this + ->getMailer( + 'CakeDC/Users.Users', + $this->_getEmailInstance($email) + ) + ->send('socialAccountValidation', [$user, $socialAccount, $template]); + } + + /** + * Get or initialize the email instance. Used for mocking. + * + * @param Email $email if email provided, we'll use the instance instead of creating a new one + * @return Email + */ + protected function _getEmailInstance(Email $email = null) + { + if ($email === null) { + $email = new Email('default'); + $email->emailFormat('both'); + } + return $email; + } +} diff --git a/src/Mailer/UsersMailer.php b/src/Mailer/UsersMailer.php new file mode 100644 index 000000000..4f51359a5 --- /dev/null +++ b/src/Mailer/UsersMailer.php @@ -0,0 +1,80 @@ +to($user['email']) + ->subject($firstName . $subject) + ->viewVars($user->toArray()) + ->template($template); + } + + /** + * Send the reset password email to the user + * + * @param EntityInterface $user User entity + * @param string $template string, note the first_name of the user will be prepended if exists + * + * @return array email send result + */ + protected function resetPassword(EntityInterface $user, $template = 'CakeDC/Users.reset_password') + { + $firstName = isset($user['first_name'])? $user['first_name'] . ', ' : ''; + $subject = __d('Users', '{0}Your reset password link', $firstName); + + $this + ->to($user['email']) + ->subject($subject) + ->viewVars($user->toArray()) + ->template($template); + } + + /** + * Send account validation email to the user + * + * @param EntityInterface $user User entity + * @param EntityInterface $socialAccount SocialAccount entity + * + * @return array email send result + */ + protected function socialAccountValidation(EntityInterface $user, EntityInterface $socialAccount) + { + $firstName = isset($user['first_name'])? $user['first_name'] . ', ' : ''; + //note: we control the space after the username in the previous line + $subject = __d('Users', '{0}Your social account validation link', $firstName); + $this + ->to($user['email']) + ->subject($subject) + ->viewVars(compact('user', 'socialAccount')); + } +} diff --git a/src/Model/Behavior/Behavior.php b/src/Model/Behavior/Behavior.php index d0307d980..75752974f 100644 --- a/src/Model/Behavior/Behavior.php +++ b/src/Model/Behavior/Behavior.php @@ -13,7 +13,6 @@ use Cake\Datasource\EntityInterface; use Cake\I18n\Time; -use Cake\Mailer\Email; use Cake\ORM\Behavior as BaseBehavior; /** @@ -21,47 +20,6 @@ */ class Behavior extends BaseBehavior { - - /** - * Send the templated email to the user - * - * @param EntityInterface $user User entity - * @param string $subject Subject, note the first_name of the user will be prepended if exists - * @param Email $email instance, if null the default email configuration with the - * Users.validation template will be used, so set a ->template() if you pass an Email - * instance - * - * @return array email send result - */ - protected function _sendEmail(EntityInterface $user, $subject, Email $email = null) - { - $firstName = isset($user['first_name'])? $user['first_name'] . ', ' : ''; - $emailInstance = $this->_getEmailInstance($email) - ->to($user['email']) - ->subject($firstName . $subject) - ->viewVars($user->toArray()); - if (empty($email)) { - $emailInstance->template('CakeDC/Users.validation'); - } - return $emailInstance->send(); - } - - /** - * Get or initialize the email instance. Used for mocking. - * - * @param Email $email if email provided, we'll use the instance instead of creating a new one - * @return Email - */ - protected function _getEmailInstance(Email $email = null) - { - if ($email === null) { - $email = new Email('default'); - $email->emailFormat('both'); - } - - return $email; - } - /** * DRY for update active and token based on validateEmail flag * diff --git a/src/Model/Behavior/PasswordBehavior.php b/src/Model/Behavior/PasswordBehavior.php index fc87c150d..6f5d62346 100644 --- a/src/Model/Behavior/PasswordBehavior.php +++ b/src/Model/Behavior/PasswordBehavior.php @@ -11,6 +11,7 @@ namespace CakeDC\Users\Model\Behavior; +use CakeDC\Users\Email\EmailSender; use CakeDC\Users\Exception\UserAlreadyActiveException; use CakeDC\Users\Exception\UserNotFoundException; use CakeDC\Users\Exception\WrongPasswordException; @@ -25,6 +26,17 @@ */ class PasswordBehavior extends Behavior { + /** + * Constructor hook method. + * + * @param array $config The configuration settings provided to this behavior. + * @return void + */ + public function initialize(array $config) + { + parent::initialize($config); + $this->Email = new EmailSender(); + } /** * Resets user token * @@ -63,7 +75,7 @@ public function resetToken($reference, array $options = []) $saveResult = $this->_table->save($user); $template = !empty($options['emailTemplate']) ? $options['emailTemplate'] : 'CakeDC/Users.reset_password'; if (Hash::get($options, 'sendEmail')) { - $this->sendResetPasswordEmail($saveResult, null, $template); + $this->Email->sendResetPasswordEmail($saveResult, null, $template); } return $saveResult; } @@ -79,28 +91,6 @@ protected function _getUser($reference) return $this->_table->findAllByUsernameOrEmail($reference, $reference)->first(); } - /** - * Send the reset password email - * - * @param EntityInterface $user User entity - * @param Email $email instance, if null the default email configuration with the - * @param string $template email template - * Users.validation template will be used, so set a ->template() if you pass an Email - * instance - * @return array email send result - */ - public function sendResetPasswordEmail(EntityInterface $user, Email $email = null, $template = 'CakeDC/Users.reset_password') - { - $firstName = isset($user['first_name'])? $user['first_name'] . ', ' : ''; - $subject = __d('Users', '{0}Your reset password link', $firstName); - return $this->_getEmailInstance($email) - ->template($template) - ->to($user['email']) - ->subject($subject) - ->viewVars($user->toArray()) - ->send(); - } - /** * Change password method * diff --git a/src/Model/Behavior/RegisterBehavior.php b/src/Model/Behavior/RegisterBehavior.php index ab95edc72..d64c17d25 100644 --- a/src/Model/Behavior/RegisterBehavior.php +++ b/src/Model/Behavior/RegisterBehavior.php @@ -11,6 +11,7 @@ namespace CakeDC\Users\Model\Behavior; +use CakeDC\Users\Email\EmailSender; use CakeDC\Users\Exception\TokenExpiredException; use CakeDC\Users\Exception\UserAlreadyActiveException; use CakeDC\Users\Exception\UserNotFoundException; @@ -40,6 +41,7 @@ public function initialize(array $config) parent::initialize($config); $this->validateEmail = (bool)Configure::read('Users.Email.validate'); $this->useTos = (bool)Configure::read('Users.Tos.required'); + $this->Email = new EmailSender(); } /** @@ -63,7 +65,7 @@ public function register($user, $data, $options) $this->_table->isValidateEmail = $validateEmail; $userSaved = $this->_table->save($user); if ($userSaved && $validateEmail) { - $this->_sendEmail($user, __d('Users', 'Your account validation link'), $emailClass); + $this->Email->sendValidationEmail($user, $emailClass); } return $userSaved; } diff --git a/src/Model/Behavior/SocialAccountBehavior.php b/src/Model/Behavior/SocialAccountBehavior.php index 8b37502b2..f6c87f01b 100644 --- a/src/Model/Behavior/SocialAccountBehavior.php +++ b/src/Model/Behavior/SocialAccountBehavior.php @@ -12,6 +12,7 @@ namespace CakeDC\Users\Model\Behavior; use ArrayObject; +use CakeDC\Users\Email\EmailSender; use CakeDC\Users\Exception\AccountAlreadyActiveException; use CakeDC\Users\Exception\AccountNotActiveException; use CakeDC\Users\Exception\MissingEmailException; @@ -45,6 +46,7 @@ public function initialize(array $config) 'joinType' => 'INNER', 'className' => Configure::read('Users.table') ]); + $this->Email = new EmailSender(); } /** @@ -77,18 +79,8 @@ public function afterSave(Event $event, Entity $entity, $options) */ public function sendSocialValidationEmail(EntityInterface $socialAccount, EntityInterface $user, Email $email = null) { - $emailInstance = $this->_getEmailInstance($email); - if (empty($email)) { - $emailInstance->template('CakeDC/Users.social_account_validation'); - } - $firstName = isset($user['first_name'])? $user['first_name'] . ', ' : ''; - //note: we control the space after the username in the previous line - $subject = __d('Users', '{0}Your social account validation link', $firstName); - return $emailInstance - ->to($user['email']) - ->subject($subject) - ->viewVars(compact('user', 'socialAccount')) - ->send(); + $this->Email = new EmailSender(); + $this->Email->sendSocialValidationEmail($socialAccount, $user, $email); } /** diff --git a/tests/TestCase/Email/EmailSenderTest.php b/tests/TestCase/Email/EmailSenderTest.php new file mode 100644 index 000000000..a08755b5b --- /dev/null +++ b/tests/TestCase/Email/EmailSenderTest.php @@ -0,0 +1,161 @@ +EmailSender = $this->getMockBuilder('CakeDC\Users\Email\EmailSender') + ->setMethods(['_getEmailInstance', 'getMailer']) + ->getMock(); + + $this->UserMailer = $this->getMockBuilder('CakeDC\Users\Mailer\UserMailer') + ->setMethods(['send']) + ->getMock(); + + $this->fullBaseBackup = Router::fullBaseUrl(); + Router::fullBaseUrl('http://users.test'); + + Email::configTransport('test', [ + 'className' => 'Debug' + ]); + } + + /** + * tearDown + * + * @return void + */ + public function tearDown() + { + Email::drop('default'); + Email::dropTransport('test'); + parent::tearDown(); + } + + /** + * test sendValidationEmail + * + * @return void + */ + public function testSendEmailValidation() + { + $table = TableRegistry::get('CakeDC/Users.Users'); + $user = $table->newEntity([ + 'first_name' => 'FirstName', + 'email' => 'test@example.com', + 'token' => '12345' + ]); + + $email = new Email([ + 'from' => 'test@example.com', + 'transport' => 'test', + 'emailFormat' => 'both', + ]); + + $this->EmailSender->expects($this->once()) + ->method('getMailer') + ->with('CakeDC/Users.Users') + ->will($this->returnValue($this->UserMailer)); + + $this->UserMailer->expects($this->once()) + ->method('send') + ->with('validation', [$user, 'Your account validation link']); + + $this->EmailSender->sendValidationEmail($user, $email); + } + + /** + * test sendResetPasswordEmail + * + * @return void + */ + public function testSendResetPasswordEmailMailer() + { + $table = TableRegistry::get('CakeDC/Users.Users'); + $user = $table->newEntity([ + 'first_name' => 'FirstName', + 'email' => 'test@example.com', + 'token' => '12345' + ]); + + $email = new Email([ + 'from' => 'test@example.com', + 'transport' => 'test', + 'template' => 'CakeDC/Users.reset_password', + 'emailFormat' => 'both', + ]); + + $this->EmailSender->expects($this->once()) + ->method('getMailer') + ->with('CakeDC/Users.Users') + ->will($this->returnValue($this->UserMailer)); + + $this->UserMailer->expects($this->once()) + ->method('send') + ->with('resetPassword', [$user, 'CakeDC/Users.reset_password']); + + $this->EmailSender->sendResetPasswordEmail($user, $email); + } + + /** + * test sendSocialValidationEmail + * + * @return void + */ + public function testSendSocialValidationEmailMailer() + { + $this->Table = TableRegistry::get('CakeDC/Users.SocialAccounts'); + $user = $this->Table->find()->contain('Users')->first(); + $email = new Email([ + 'from' => 'test@example.com', + 'transport' => 'test', + 'template' => 'CakeDC/Users.my_template', + 'emailFormat' => 'both', + ]); + + $this->EmailSender->expects($this->once()) + ->method('getMailer') + ->with('CakeDC/Users.Users') + ->will($this->returnValue($this->UserMailer)); + + $this->UserMailer->expects($this->once()) + ->method('send') + ->with('socialAccountValidation', [$user->user, $user, 'CakeDC/Users.my_template']); + + $this->EmailSender->sendSocialValidationEmail($user, $user->user, $email); + } +} diff --git a/tests/TestCase/Mailer/UsersMailerTest.php b/tests/TestCase/Mailer/UsersMailerTest.php new file mode 100644 index 000000000..1aae78b62 --- /dev/null +++ b/tests/TestCase/Mailer/UsersMailerTest.php @@ -0,0 +1,221 @@ +Email = $this->getMockBuilder('Cake\Mailer\Email') + ->setMethods(['to', 'subject', 'viewVars', 'template']) + ->getMock(); + + $this->UsersMailer = $this->getMockBuilder('CakeDC\Users\Mailer\UsersMailer') + ->setConstructorArgs([$this->Email]) + ->setMethods(['to', 'subject', 'viewVars', 'template']) + ->getMock(); + } + + /** + * tearDown + * + * @return void + */ + public function tearDown() + { + unset($this->UsersMailer); + unset($this->Email); + parent::tearDown(); + } + + /** + * test sendValidationEmail + * + * @return void + */ + public function testValidation() + { + $table = TableRegistry::get('CakeDC/Users.Users'); + $data = [ + 'first_name' => 'FirstName', + 'email' => 'test@example.com', + 'token' => '12345' + ]; + $user = $table->newEntity($data); + $this->UsersMailer->expects($this->once()) + ->method('to') + ->with($user['email']) + ->will($this->returnValue($this->Email)); + + $this->Email->expects($this->once()) + ->method('subject') + ->with('FirstName, Validate your Account') + ->will($this->returnValue($this->Email)); + + $this->Email->expects($this->once()) + ->method('viewVars') + ->with($data) + ->will($this->returnValue($this->Email)); + + $this->Email->expects($this->once()) + ->method('template') + ->with('CakeDC/Users.validation') + ->will($this->returnValue($this->Email)); + + $this->invokeMethod($this->UsersMailer, 'validation', [$user, 'Validate your Account']); + } + + /** + * test sendValidationEmail including 'template' + * + * @return void + */ + public function testValidationWithTemplate() + { + $table = TableRegistry::get('CakeDC/Users.Users'); + $data = [ + 'first_name' => 'FirstName', + 'email' => 'test@example.com', + 'token' => '12345' + ]; + $user = $table->newEntity($data); + $this->UsersMailer->expects($this->once()) + ->method('to') + ->with($user['email']) + ->will($this->returnValue($this->Email)); + + $this->Email->expects($this->once()) + ->method('subject') + ->with('FirstName, Validate your Account') + ->will($this->returnValue($this->Email)); + + $this->Email->expects($this->once()) + ->method('viewVars') + ->with($data) + ->will($this->returnValue($this->Email)); + + $this->Email->expects($this->once()) + ->method('template') + ->with('myTemplate') + ->will($this->returnValue($this->Email)); + + $this->invokeMethod($this->UsersMailer, 'validation', [$user, 'Validate your Account', 'myTemplate']); + } + + /** + * test SocialAccountValidation + * + * @return void + */ + public function testSocialAccountValidation() + { + $social = TableRegistry::get('CakeDC/Users.SocialAccounts') + ->get('00000000-0000-0000-0000-000000000001', ['contain' => 'Users']); + + $this->UsersMailer->expects($this->once()) + ->method('to') + ->with('user-1@test.com') + ->will($this->returnValue($this->Email)); + + $this->Email->expects($this->once()) + ->method('subject') + ->with('first1, Your social account validation link') + ->will($this->returnValue($this->Email)); + + + $this->Email->expects($this->once()) + ->method('viewVars') + ->with(['user' => $social->user, 'socialAccount' => $social]) + ->will($this->returnValue($this->Email)); + + $this->invokeMethod($this->UsersMailer, 'socialAccountValidation', [$social->user, $social]); + } + + /** + * test sendValidationEmail including 'template' + * + * @return void + */ + public function testResetPassword() + { + $table = TableRegistry::get('CakeDC/Users.Users'); + $data = [ + 'first_name' => 'FirstName', + 'email' => 'test@example.com', + 'token' => '12345' + ]; + $user = $table->newEntity($data); + $this->UsersMailer->expects($this->once()) + ->method('to') + ->with($user['email']) + ->will($this->returnValue($this->Email)); + + $this->Email->expects($this->once()) + ->method('subject') + ->with('FirstName, Your reset password link') + ->will($this->returnValue($this->Email)); + + $this->Email->expects($this->once()) + ->method('viewVars') + ->with($data) + ->will($this->returnValue($this->Email)); + + $this->Email->expects($this->once()) + ->method('template') + ->with('myTemplate') + ->will($this->returnValue($this->Email)); + + + $this->invokeMethod($this->UsersMailer, 'resetPassword', [$user, 'myTemplate']); + } + + /** + * Call protected/private method of a class. + * + * @param object &$object Instantiated object that we will run method on. + * @param string $methodName Method name to call + * @param array $parameters Array of parameters to pass into method. + * + * @return mixed Method return. + */ + public function invokeMethod(&$object, $methodName, $parameters = []) + { + $reflection = new \ReflectionClass(get_class($object)); + $method = $reflection->getMethod($methodName); + $method->setAccessible(true); + + return $method->invokeArgs($object, $parameters); + } +} diff --git a/tests/TestCase/Model/Behavior/PasswordBehaviorTest.php b/tests/TestCase/Model/Behavior/PasswordBehaviorTest.php index f782e0d0c..238a7923d 100644 --- a/tests/TestCase/Model/Behavior/PasswordBehaviorTest.php +++ b/tests/TestCase/Model/Behavior/PasswordBehaviorTest.php @@ -46,6 +46,9 @@ public function setUp() ->setMethods(['sendResetPasswordEmail']) ->setConstructorArgs([$this->table]) ->getMock(); + $this->Behavior->Email = $this->getMockBuilder('CakeDC\Users\Email\EmailSender') + ->setMethods(['sendResetPasswordEmail']) + ->getMock(); } /** @@ -68,7 +71,7 @@ public function testResetToken() { $user = $this->table->findAllByUsername('user-1')->first(); $token = $user->token; - $this->Behavior->expects($this->never()) + $this->Behavior->Email->expects($this->never()) ->method('sendResetPasswordEmail') ->with($user); $result = $this->Behavior->resetToken('user-1', [ @@ -89,7 +92,7 @@ public function testResetTokenSendEmail() $user = $this->table->findAllByUsername('user-1')->first(); $token = $user->token; $tokenExpires = $user->token_expires; - $this->Behavior->expects($this->once()) + $this->Behavior->Email->expects($this->once()) ->method('sendResetPasswordEmail'); $result = $this->Behavior->resetToken('user-1', [ 'expiration' => 3600, @@ -143,70 +146,4 @@ public function testResetTokenUserAlreadyActive() 'checkActive' => true, ]); } - - /** - * Test method - * - * @return void - */ - public function testSendResetPasswordEmail() - { - $behavior = $this->table->behaviors()->Password; - $this->fullBaseBackup = Router::fullBaseUrl(); - Router::fullBaseUrl('http://users.test'); - Email::configTransport('test', [ - 'className' => 'Debug' - ]); - $this->Email = new Email([ - 'from' => 'test@example.com', - 'transport' => 'test', - 'template' => 'CakeDC/Users.reset_password', - 'emailFormat' => 'both', - ]); - - $user = $this->table->newEntity([ - 'first_name' => 'FirstName', - 'email' => 'test@example.com', - 'token' => '12345' - ]); - - $result = $behavior->sendResetPasswordEmail($user, $this->Email, 'CakeDC/Users.reset_password'); - $this->assertTextContains('From: test@example.com', $result['headers']); - $this->assertTextContains('To: test@example.com', $result['headers']); - $this->assertTextContains('Subject: FirstName, Your reset password link', $result['headers']); - $this->assertTextContains('Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit - -Hi FirstName, - -Please copy the following address in your web browser http://users.test/users/users/reset-password/12345 -Thank you, -', $result['message']); - $this->assertTextContains('Content-Type: text/html; charset=UTF-8 -Content-Transfer-Encoding: 8bit - - - - - Email/html - - -

-Hi FirstName, -

-

- Reset your password here -

-

- If the link is not correcly displayed, please copy the following address in your web browser http://users.test/users/users/reset-password/12345

-

- Thank you, -

- - -', $result['message']); - - Router::fullBaseUrl($this->fullBaseBackup); - Email::dropTransport('test'); - } } diff --git a/tests/TestCase/Model/Behavior/SocialAccountBehaviorTest.php b/tests/TestCase/Model/Behavior/SocialAccountBehaviorTest.php index 2c0344bb7..8bfc5b5a5 100644 --- a/tests/TestCase/Model/Behavior/SocialAccountBehaviorTest.php +++ b/tests/TestCase/Model/Behavior/SocialAccountBehaviorTest.php @@ -44,16 +44,6 @@ public function setUp() parent::setUp(); $this->Table = TableRegistry::get('CakeDC/Users.SocialAccounts'); $this->Behavior = $this->Table->behaviors()->SocialAccount; - $this->fullBaseBackup = Router::fullBaseUrl(); - Router::fullBaseUrl('http://users.test'); - Email::configTransport('test', [ - 'className' => 'Debug' - ]); - $this->Email = new Email([ - 'from' => 'test@example.com', - 'transport' => 'test', - 'template' => 'CakeDC/Users.social_account_validation', - ]); } /** @@ -64,8 +54,6 @@ public function setUp() public function tearDown() { unset($this->Table, $this->Behavior, $this->Email); - Router::fullBaseUrl($this->fullBaseBackup); - Email::dropTransport('test'); parent::tearDown(); } @@ -150,22 +138,4 @@ public function testAfterSaveSocialActiveUserNotActive() $entity = $this->Table->findById('00000000-0000-0000-0000-000000000002')->first(); $this->assertTrue($this->Behavior->afterSave($event, $entity, [])); } - - /** - * Test sendSocialValidationEmail method - * - * @return void - */ - public function testSendSocialValidationEmail() - { - $user = $this->Table->find()->contain('Users')->first(); - $this->Email->emailFormat('both'); - $result = $this->Behavior->sendSocialValidationEmail($user, $user->user, $this->Email); - $this->assertTextContains('From: test@example.com', $result['headers']); - $this->assertTextContains('To: user-1@test.com', $result['headers']); - $this->assertTextContains('Subject: first1, Your social account validation link', $result['headers']); - $this->assertTextContains('Hi first1,', $result['message']); - $this->assertTextContains('Activate your social login here', $result['message']); - $this->assertTextContains('If the link is not correcly displayed, please copy the following address in your web browser http://users.test/users/social-accounts/validate-account/Facebook/reference-1-1234/token-1234', $result['message']); - } }