Unit Testing Mail Queue on Laravel 5 - php

I'm facing two situations that I would like to address / understand.
1st - How do I Unit Test Laravel's Mail Queue Class?
The code that I want to test is this:
// Create new customer record
$account = $this->create(['account_id' => $account->id]);
// Get email address to send welcome email.
$email = $data['email'];
// Email Subject
$subject = $this->word('emails.welcome.subject');
$this->mailQueue->queue('emails.welcome',
['some_data' => 'data'],
function ($message) use ($email, $subject) {
$message->to($email)->subject($subject);
}, true);
return $account;
I would like to know where is the shouldReceive method that will work for me when using Illuminate\Contracts\Mail\MailQueue class.
Right now I have this unit test for this:
/**
* #tests
*/
public function it_should_sign_up_a_new_user() {
// MailQueue::shouldReceive() does not exist.
list($account, $email) = $this->getAccountData();
$request = array_merge($account, $email);
$account['password'] = $this->hash($account['password']);
$this->post('/signup', $request, $this->header)
->assertResponseOk()
->seeInDatabase('account', $account)
->seeInDatabase('email', $email);
}
2nd - Why Unit Test does not require php artisan queue:listen or queue:work?
Every time I run the Unit Test, the email gets dispatched even though I have no queue:listen running. I would like to understand how this awesome magic happens.

Per the docs it looks like they recommend using Mail::queue . i.e.
Mail::queue('emails.welcome', $data, function($message)
{
$message->to('foo#example.com', 'John Smith')->subject('Welcome!');
});
The Mail Facade has shouldReceive built into it so you should be able to do:
Mail::shouldReceive('queue')->once()
https://laravel.com/docs/5.0/mail#queueing-mail

Related

Laravel Schedule not sending email

It's my first time trying to implement Task Scheduling, I'm trying to send automatic E-mails at a certain time:
Before implementing my cron I first tested my email sending code manually in a normal class to see if there is no error, and there was no error, the email was sent successfully.
After that, I started implementing the Task Scheduling
Democron.php
protected $signature = 'demo:cron';
protected $description = 'Command description';
public function __construct()
{
parent::__construct();
}
public function handle()
{
$tasks = Task::all();
$date = Carbon::now()->toDateTimeString();
foreach ($tasks as $task) {
if($task->completed_at != null){
$validad = $task->completed_at;
$receiver_id = User::findOrFail($task->user_id);
if($date > $validad){
$details = [
'task_id' =>$task->id,
'receiver_id' => $receiver_id
];
$subject = 'TeamWork - Você tem tarefas em atraso!';
$view = 'emails.project.delaydtask';
Mail::to($receiver_id->email)->send(new SendMail($details, $subject, $view));
Log::info('Email enviado com sucesso para '.$receiver_id->email);
}
}
}
}
Kernel.php
protected $commands = [
DemoCron::class,
];
protected function schedule(Schedule $schedule)
{
$schedule->command('demo:cron')
->twiceDaily(12, 15)
->timezone('Africa/Maputo');
}
protected function commands()
{
$this->load(__DIR__.'/Commands');
require base_path('routes/console.php');
}
I added to CRON JOBS on CPANEL
and set twiceDaily at 12 and 15
/usr/local/bin/php /.......myProjectPath/artisan schedule:run >> /dev/null 2>&1
I printed a LOG in my DemoCron.php to see if it really works
Result 1: when I select schedule once per minute it prints my LOG respecting all the conditions that are in my Democron.php , but it doesn't send the email.
Result 2: When I select a certain time (Twice per day or once a day) my LOG does not print anything and it does not send the email.
What am I doing wrong? Help me please!
UPDATE
my SendMail class that i use to send emails manually works perfectly,
but the scheduled emails are not going
class SendMail extends Mailable
{
use Queueable, SerializesModels;
public $details, $subject, $view;
public function __construct($details, $subject, $view)
{
$this->details = $details;
$this->subject = $subject;
$this->view = $view;
}
public function build()
{
return $this->subject($this->subject)
->view($this->view, ['details' => $this->details]);
}
}
After trying several times I found a workaround.
1- create a new controller
I created a new controller called MailController instead of using the Kernel.php and Democron.php classes that I generated through Laravel Scheduling
class MailController extends Controller
{
public function delayedtask(){
try {
$tasks = Task::all();
$date = Carbon::now()->toDateTimeString();
foreach ($tasks as $task) {
if($task->completed_at != null){
$validad = $task->completed_at;
$receiver_id = User::findOrFail($task->user_id);
if($date > $validad){
$details = [
'task_id' =>$task->id,
'receiver_id' => $receiver_id
];
$subject = 'TeamWork - Você tem tarefas em atraso!';
$view = 'emails.project.delaydtask';
Mail::to($receiver_id->email)->send(new SendMailQueue($details, $subject, $view));
Log::info('Email enviado com sucesso para '.$receiver_id->email);
}
}
}
return "Done!";
} catch (Exception $e) {
return "Something went wrong!";
}
}
}
2-add a new route
added a new route without Auth
Route::get('/delayedtask',[MailController::class, 'delayedtask']);
3-Added a cronjob on Cpanel
curl -s "https://myWebsiteURL/delayedtask">/dev/null 2>&1
First of all lets check all things:
Verify your mail configurations in your .env;
Verify in your email class if have implements ShouldQueue;
If you are implementing ShouldQueue, you must have to verify too your queue´s configuration in .env;
If is not implementing ShouldQueue, don´t miss time verifying queue´s config;
All right all things validated and still not sending email:
Add the Send mail in try catch and log the catch if something went wrong;
If don´t log nothing in try catch, try to create an command that just send a simple email;
If dosen´t work try to send an email by your mail in Cpanel, because this should be the problem;
Finally
In my cases using cPanel, I always create the croon task to all seconds like * * * * * and in the kernel of my laravel project I verify if some command must be executed with the laravel commands like ->twiceDaily(12, 15).
Try all things and if the error still, please update this thread!
I had the same problem,
i tried a new smtp email server
MAIL_HOST=pro.eu.turbo-smtp.com
MAIL_ENCRYPTION=ssl
instead of
MAIL_HOST=smtpauth.online.net
MAIL_ENCRYPTION=tls
I don't know if it's about the encryption or host features,
but it worked for me

Laravel Reset Password Notification does not get dispatched in test but does send an email

I am writing tests for my Laravel project. Right now I am testing the authentication code like login, logout, reset password and so on.
Sadly, my test is failing because there is no notification send. I have mocked the notifications but assertSendTo always fails with the reason The expected [Illuminate\Auth\Notifications\ResetPassword] notification was not sent..
However, when actual requesting a reset password email (not in the test, as a normal user on my website) I indeed do get an reset password email. So, it is functional and working but not in my test. How can this be? The .evn is also correct, I have set my mail host to mailtrap.io and I also receive this email... This is the best proof I can give you.
Here is my test:
use App\Models\User;
use Illuminate\Auth\Notifications\ResetPassword;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Str;
use Tests\TestCase;
class AuthTest extends TestCase
{
/** #test */
public function a_not_logged_in_user_can_request_a_new_password()
{
Notification::fake();
$email = Str::random() . "#gmail.com";
$current_password = Str::random(16);
$current_password_hash = Hash::make($current_password);
$user = User::factory()->create([
'email' => $email,
'password' => $current_password_hash
]);
$response = $this->json('POST', route('password.email'), ['email' => $email]);
$response->assertStatus(200);
$response->assertLocation(route('home'));
//$this->expectsNotification($user, ResetPassword::class);
Notification::assertSentTo($user, ResetPassword::class);
}
}
Any ideas why the test is not working or whats wrong with it?
Whats also very strange is the fact that the response code 200 is indicating that everything succeeded without any problem.
This is the error I get when executing the test with assertSentTo
1) Tests\Feature\Auth\LoggedIn\ForgotPassword\AuthTest::a_not_logged_in_user_can_request_a_new_password
The expected [Illuminate\Auth\Notifications\ResetPassword] notification was not sent.
Failed asserting that false is true.
MyWebsiteProject/vendor/laravel/framework/src/Illuminate/Support/Testing/Fakes/NotificationFake.php:68
/MyWebsiteProject/vendor/laravel/framework/src/Illuminate/Support/Facades/Facade.php:261
MyWebsiteProject/tests/Feature/Auth/LoggedIn/ForgotPassword/AuthTest.php:35
And this is the error I get when executing it with expectsNotification
1) Tests\Feature\Auth\LoggedIn\ForgotPassword\AuthTest::a_logged_in_user_can_request_a_new_password
The following expected notification were not dispatched: [Illuminate\Auth\Notifications\ResetPassword]
MyWebsiteProject/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/MocksApplicationServices.php:281
MyWebsiteProject/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestCase.php:237
MyWebsiteProject/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestCase.php:153
Kind regards and thank you!
My app was initially created in L5 when the default User.php fully qualified class name was
App\User.php but, according to L8 new pattern, I've duplicated it (to avoid refactoring.... my fault) to App\Models\User.php.
https://twitter.com/taylorotwell/status/1296556354593792000
This partial misalignment, though, left some of my tests behind, expecially the ones involving fake-notifications in which UserFactory kept dispatching (via NotificationFake.php) to App\User.php instead of App\Models\User.php and determining the assertSentTo to fail.
TLDR
make sure that config/auth.php (param providers.users.model) and UserFactory.php rely on the same model.
Expects does just that "expect that something is going to happen", in your case you are expecting it after the fact. The latter is the use case for an assertion. Something like the following is what you need.
class AuthTest extends TestCase
{
/** #test */
public function a_not_logged_in_user_can_request_a_new_password()
{
$email = Str::random() . "#gmail.com";
$current_password = Str::random(16);
$current_password_hash = Hash::make($current_password);
$user = User::factory()->create([
'email' => $email,
'password' => $current_password_hash
]);
// Expect that something is going to happen
$this->expectsNotification($user, ResetPassword::class);
$response = $this->json('POST', route('password.email'), ['email' => $email]);
// Assertion that something has happened
$response->assertStatus(200);
$response->assertLocation(route('home'));
}
}
In case non of the above solutions works for you like in my case, try adding this line at the top of your test function;
$this->withoutExceptionHandling();
This will give you a good error message as to where the problem might be coming from. In my case, I was using the wrong route.

Laravel: Use Email and Name in Mail::to

I have a contact form where someone provides his name and email. I want to send him an email now with Laravel.
I found in the docs
To send a message, use the to method on the Mail facade. The to method
accepts an email address, a user instance, or a collection of users.
and in fact
\Mail::to('example#gmail.com')->send(new \App\Mail\Hello);
works. But is it also possible to provide the name for the email receipt?
I wanted to look that up in the Laravel API for the Mail Facade but to my surprise the facade has no to function?
So how can I find out what the to function really does and if I can pass a name parameter as well?
In laravel 5.6, answer to your question is: use associative array for every recpient with 'email' and 'name' keys, should work with $to, $cc, $bcc
$to = [
[
'email' => $email,
'name' => $name,
]
];
\Mail::to($to)->send(new \App\Mail\Hello);
You can use the Mail::send() function that inject a Message class in the callable. The Message class has a function to($email, $name) with the signature you're searching, i.e.:
Mail::send($view, $data, function($message) use ($email, $name) {
$m->to($email, $name);
$m->from('youremail#example.com', 'Your Name');
$m->subject('Hi there');
})
The $view could be a string (an actual view) or an array like these:
['text'=> 'body here']
['html'=> 'body here']
['raw'=> 'body here']
The $data argument will be passed to the $view.
For Laravel < 5.6 one can use this:
$object = new \stdClass();
$object->email = $email;
$object->name = $user->getName();
\Mail::to($object)->queue($mailclass);
see here
I prefer this solution as more readable (no need to use arrays and static string keys).
\Mail::send((new \App\Mail\Hello)
->to('example#gmail.com', 'John Doe');
You can use Mailable class in Laravel:
https://laravel.com/docs/5.5/mail
php artisan make:mail YouMail
These classes are stored in the app/Mail directory.
In config/mail.php you can configue email settings:
'from' => ['address' => 'example#example.com', 'name' => 'App Name'],
for Laravel 8 is like this:
$user = new User;
$user->email = 'example#example.com';
Mail::to($user)->send(new YourMail);
YourMail is Mailable class created by php artisan make:mail YourMail

Symfony Swiftmailer stop sending process to show status

In our application, for some actions we send out notifications and emails to a rather large number of users (several hundred to thousand). For some purposes we have to send these emails/notifications separately which I did, using a foreach loop.
For about 200-300 users, that's working fine, but as soon as there are more users to be notified, we get a timeout after a while.
What I was now thinking to do, is redirect to a new page, after e.g. a document is created, and handle the email/notification send out there by sending out let's say 20 emails, then display an update like "20 out of xxx emails/notifications have been sent out", then continue sending the next 20, displaying an update and so on.
Is there any body who already did something like this or has ideas on that?
Here is my code so far:
{
document gets created...
$em->persist($document);
$em->flush();
$this->addFlash(
'success',
'Your document has been created!'
);
return $this->redirectToRoute('documentBundle_document_send_notifications', array('id' => strtok($document->getId(), '_')));
}
/**
* #Route("/document/sendNotifications/{id}", name="documentBundle_document_send_notifications", requirements={"id" = "\d+"})
*/
public function sendDocumentNotifications($id){
$em = $this->getDoctrine()->getManager();
$document = $em->getRepository('DocumentBundle:Document')->findOneById($id);
$statusRepository = $em->getRepository('DocumentBundle:Status');
/*
* NOTIFICATION MANAGEMENT
*/
//Users
$user = $em->getRepository('UserBundle:User');
$user = $user->findByDocumentAgency($user, $document);
$users = array_unique($user->getResult());
if(count($users)>0){
/*
* SEND NOTIFICATION
*/
foreach ($users as $user){
$manager = $this->get('mgilet.notification');
$notif = $manager->generateNotification('A Document has been created!');
$manager->addNotification($user, $notif);
}
/*
* SEND EMAIL
*/
foreach ($users as $user){
$recipient = $user->getEmail();
$this->get('MailerHelper')->sendMessage($recipient,...);
}
}
}
return $this->redirectToRoute('documentBundle_document_list');
}
EDIT
added a console command to run as a background process
/**
* #Route("/document/sendNotifications/{id}", name="documentBundle_document_send_notifications", requirements={"id" = "\d+"})
*/
public function sendDocumentNotifications($id){
$em = $this->getDoctrine()->getManager();
$document = $em->getRepository('DocumentBundle:Document')->findOneById($id);
$process = new Process('php app/console app:send-notifications', $id );
$process->run();
dump($process);
return $this->redirectToRoute('documentBundle_document_bulkDeactivate');
}
when dumping the process, I get the correct commandLine and the input is the correct document Id, but it says:
Process {#1539 ▼
-callback: null
-commandline: "php app/console app:send-notifications"
-cwd: "/srv/http/sp/web"
-env: null
-input: "320"
-starttime: 1511378616.9664
-lastOutputTime: 1511378616.9852
-timeout: 60.0
-idleTimeout: null
-options: array:2 [▼
"suppress_errors" => true
"binary_pipes" => true
]
-exitcode: 1
-fallbackStatus: []
-processInformation: array:8 [▼
"command" => "php app/console app:send-notifications"
"pid" => 29887
"running" => false
"signaled" => false
"stopped" => false
"exitcode" => 1
"termsig" => 0
"stopsig" => 0
I then tried it with
$process->mustRun() and gut the following error:
The command "php app/console app:send-notifications" failed.
Exit Code: 1(General error)
Working directory: /srv/http/sp/web
Output:
================ Could not open input file: app/console
Error Output:
500 Internal Server Error - ProcessFailedException
TL;DR: to prevent time-outs, offload your process to a background process.
First step: use a spool
A quick win would be to Spool Emails:
When you are using the SwiftmailerBundle to send an email from a
Symfony application, it will default to sending the email immediately.
You may, however, want to avoid the performance hit of the
communication between Swift Mailer and the email transport, which
could cause the user to wait for the next page to load while the email
is sending.
The actual mails will be sent by a command line script:
php bin/console swiftmailer:spool:send --env=prod.
Next step: create a command that handles notifications
Learn how to create a console command. Something like this (not tested, just as an example):
class NotificationCommand extends ContainerAwareCommand
{
protected function configure()
{
$this
// the name of the command (the part after "bin/console")
->setName('app:send-notifications')
// the short description shown while running "php bin/console list"
->setDescription('Send notifications to information users about a new document.')
->addArgument('document', InputArgument::REQUIRED, 'The ID of the new document.')
;
}
public function execute(InputInterface $input, OutputInterface $output)
{
$em = $this->getContainer()->get('doctrine')->getManager();
$document = $em->getRepository('DocumentBundle:Document')->findOneById($input->getArgument('document'));
$statusRepository = $em->getRepository('DocumentBundle:Status');
/*
* NOTIFICATION MANAGEMENT
*/
//Users
$user = $em->getRepository('UserBundle:User');
$user = $user->findByDocumentAgency($user, $document);
$users = array_unique($user->getResult());
if(count($users)>0){
/*
* SEND NOTIFICATION
*/
foreach ($users as $user){
$manager = $this->get('mgilet.notification');
$notif = $manager->generateNotification('A Document has been created!');
$manager->addNotification($user, $notif);
}
/*
* SEND EMAIL
*/
foreach ($users as $user){
$recipient = $user->getEmail();
$this->get('MailerHelper')->sendMessage($recipient,...);
}
}
}
}
}
To execute this, run bin/console app:send-notifications 1 on your command line.
Schedule your command
I guess you don't want to log into the server manually to send the notifications, so instead you can create a general 'send notifications if needed' command that can scheduled by a cron job.
As a next step you can take a look at a way to queue your commands. Take JMSJobQueueBundle for example.
Bonus tip: start by making your controllers thinner
Try to make your controllers thinner. Read this old, but still interesting blog. If you create services and call them from the controller (instead of having a fat controller), the 'refactoring' job of removing code from controller to background job (and maybe visa versa) is much easier.

Laravel unit testing emails

My system sends a couple of important emails. What is the best way to unit test that?
I see you can put it in pretend mode and it goes in the log. Is there something to check that?
There are two options.
Option 1 - Mock the mail facade to test the mail is being sent. Something like this would work:
$mock = Mockery::mock('Swift_Mailer');
$this->app['mailer']->setSwiftMailer($mock);
$mock->shouldReceive('send')->once()
->andReturnUsing(function($msg) {
$this->assertEquals('My subject', $msg->getSubject());
$this->assertEquals('foo#bar.com', $msg->getTo());
$this->assertContains('Some string', $msg->getBody());
});
Option 2 is much easier - it is to test the actual SMTP using MailCatcher.me. Basically you can send SMTP emails, and 'test' the email that is actually sent. Laracasts has a great lesson on how to use it as part of your Laravel testing here.
"Option 1" from "#The Shift Exchange" is not working in Laravel 5.1, so here is modified version using Proxied Partial Mock:
$mock = \Mockery::mock($this->app['mailer']->getSwiftMailer());
$this->app['mailer']->setSwiftMailer($mock);
$mock
->shouldReceive('send')
->withArgs([\Mockery::on(function($message)
{
$this->assertEquals('My subject', $message->getSubject());
$this->assertSame(['foo#bar.com' => null], $message->getTo());
$this->assertContains('Some string', $message->getBody());
return true;
}), \Mockery::any()])
->once();
For Laravel 5.4 check Mail::fake():
https://laravel.com/docs/5.4/mocking#mail-fake
If you just don't want the e-mails be really send, you can turn off them using the "Mail::pretend(true)"
class TestCase extends Illuminate\Foundation\Testing\TestCase {
private function prepareForTests() {
// e-mail will look like will be send but it is just pretending
Mail::pretend(true);
// if you want to test the routes
Route::enableFilters();
}
}
class MyTest extends TestCase {
public function testEmail() {
// be happy
}
}
If any one is using docker as there development environment I end up solving this by:
Setup
.env
...
MAIL_FROM = noreply#example.com
MAIL_DRIVER = smtp
MAIL_HOST = mail
EMAIL_PORT = 1025
MAIL_URL_PORT = 1080
MAIL_USERNAME = null
MAIL_PASSWORD = null
MAIL_ENCRYPTION = null
config/mail.php
# update ...
'port' => env('MAIL_PORT', 587),
# to ...
'port' => env('EMAIL_PORT', 587),
(I had a conflict with this environment variable for some reason)
Carrying on...
docker-compose.ymal
mail:
image: schickling/mailcatcher
ports:
- 1080:1080
app/Http/Controllers/SomeController.php
use App\Mail\SomeMail;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller as BaseController;
class SomeController extends BaseController
{
...
public function getSomething(Request $request)
{
...
Mail::to('someone#example.com')->send(new SomeMail('Body of the email'));
...
}
app/Mail/SomeMail.php
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class SomeMail extends Mailable
{
use Queueable, SerializesModels;
public $body;
public function __construct($body = 'Default message')
{
$this->body = $body;
}
public function build()
{
return $this
->from(ENV('MAIL_FROM'))
->subject('Some Subject')
->view('mail.someMail');
}
}
resources/views/mail/SomeMail.blade.php
<h1>{{ $body }}</h1>
Testing
tests\Feature\EmailTest.php
use Tests\TestCase;
use Illuminate\Http\Request;
use App\Http\Controllers\SomeController;
class EmailTest extends TestCase
{
privete $someController;
private $requestMock;
public function setUp()
{
$this->someController = new SomeController();
$this->requestMock = \Mockery::mock(Request::class);
}
public function testEmailGetsSentSuccess()
{
$this->deleteAllEmailMessages();
$emails = app()->make('swift.transport')->driver()->messages();
$this->assertEmpty($emails);
$response = $this->someController->getSomething($this->requestMock);
$emails = app()->make('swift.transport')->driver()->messages();
$this->assertNotEmpty($emails);
$this->assertContains('Some Subject', $emails[0]->getSubject());
$this->assertEquals('someone#example.com', array_keys($emails[0]->getTo())[0]);
}
...
private function deleteAllEmailMessages()
{
$mailcatcher = new Client(['base_uri' => config('mailtester.url')]);
$mailcatcher->delete('/messages');
}
}
(This has been copied and edited from my own code so might not work first time)
(source: https://stackoverflow.com/a/52177526/563247)
I think that inspecting the log is not the good way to go.
You may want to take a look at how you can mock the Mail facade and check that it receives a call with some parameters.
if you are using Notifcations in laravel you can do that like below
Notification::fake();
$this->post(...);
$user = User::first();
Notification::assertSentTo([$user], VerifyEmail::class);
https://laravel.com/docs/7.x/mocking#notification-fake
If you want to test everything around the email, use
Mail::fake()
But if you want to test your Illuminate\Mail\Mailable and the blade, then follow this example. Say, you want to test a Reminder email about some payment, where the email text should have product called 'valorant' and some price in 'USD'.
public function test_PaymentReminder(): void
{
/* #var $payment SalePayment */
$payment = factory(SalePayment::class)->create();
auth()->logout();
$paymentReminder = new PaymentReminder($payment);
$html = $paymentReminder->render();
$this->assertTrue(strpos($html, 'valorant') !== false);
$this->assertTrue(strpos($html, 'USD') !== false);
}
The important part here is ->render() - that is how you make Illuminate\Mail\Mailable to run build() function and process the blade.
Another importan thing is auth()->logout(); - because normally emails being processed in a queue that run in a background environment. This environment has no user and has no request with no URL and no IP...
So you must be sure that you are rendering the email in your unit test in a similar environment as in production.

Categories