"Unable to open file for reading" (Swift_IoException) in Laravel Mailable - php

I'm trying to use Mailable in Laravel.
In developing a new Mailable, I have everything working except attaching an EXISTING file to the mailable.
An error returns as such:
"message": "Unable to open file for reading [/public/storage/shipments/CJ2K4u6S6uluEGd8spOdYgwNkg8NgLFoC6cF6fm5.pdf]",
"exception": "Swift_IoException",
"file": "E:\\webserver\\htdocs\\truckin\\vendor\\swiftmailer\\swiftmailer\\lib\\classes\\Swift\\ByteStream\\FileByteStream.php",
"line": 131,
But if you go through the folders and files, there is in fact a file there and I can open it, I can even open it through an ajax popup to view details.
Here is my mailable:
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue;
use App\Shipment;
use App\Shipment_Attachment;
class shipmentAttachments extends Mailable
{
use Queueable, SerializesModels;
/**
* Create a new message instance.
*
* #return void
*/
public $shipment, $attachment, $storagePath;
public function __construct($shipment, $attachment, $storagePath)
{
$this->shipment = $shipment;
$this->attachment = $attachment;
$this->attachmentFile = '/public'.$storagePath;
$this->proNumber = $shipment->pro_number;
}
/**
* Build the message.
*
* #return $this
*/
public function build()
{
return $this->from('billing#cmxtrucking.com')
->cc('billing#cmxtrucking.com')
->subject('New Attachment(s) - '. $this->proNumber)
->view('emails.shipments.shipmentAttachments',['shipment'=> $this->shipment])
->attach($this->attachmentFile);
}
}
And here is my controller that leads to the mailable:
public function attachmentsEmail(Request $request){
$shipment = Shipment::findOrFail($request->shipmentID);
$attachment = Shipment_Attachment::findOrFail($request->attachmentID);
$storagePath = Storage::url($attachment->attachmentPath);
$email = $request->email;
Mail::to($email)->send(new shipmentAttachments($shipment, $attachment, $storagePath)); //maybe try to use queue instead of send...
return back();
}
So I'm not sure where this could be coming from.

Try to use public_path() laravel helper function instead of '/public'.
$this->attachmentFile = public_path() . '/' . $storagePath;
Maybe you need to change this variable in public/index.php. I have right below the require bootstrap:
$app->bind('path.public', function() {
return __DIR__;
});
Make some tests.
dd(public_path());
dd(public_path() . '/' . $storagePath);
Or maybe verify if file exist with FileSystem class.
Hope this help you!

I was serching a lot about that, it happens the same when you are tryng to build a PDF on dompdf, just exactly the same, you normaly could write this:
('/image/'.$file) and will not work , so you can solve it adding a dot just behind the rout ".", just like this:
('./image/'.$file)
It works when you want to add a attach in a mail sending or when you want to make a PDF including images in it.

If you use Storage, and you are trying to export xlsx files, using Laravel Notifications:
in your notification class:
public function toMail($notifiable) {
$path = Storage::disk('export')->getAdapter()->getPathPrefix();
return (new MailMessage)
->greeting(language_data('Your file is ready', $this->user->language_id).$this->user->name)
->line(language_data('Please, check your Email attachments.', $this->user->language_id))
->subject(language_data('Export Contacts', $this->user->language_id))
->attach($path.$notifiable->filename, [ 'as' => $notifiable->filename, 'mime' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ])
->line(language_data('If you did not request this file, please contact us.', $this->user->language_id));
}
It works fine for me.

Instead of /public we have to use laravel's helper function public_path()
then concatenate actual file path. otherwise the attachment file operation will not work
so your updated code should be like:-
$this->attachmentFile = public_path() . '/' . $storagePath;
Most of these errors occur in larval on Ubuntu (Linux).
It may be skipped in some cases of windows.

Related

Laravel mail queue subject not being applied

I always have lots of problems with Mail::queue and this time the subject is not being applied properly.
This is my class:
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class PlanExpiringOrExpired extends Mailable
{
use Queueable, SerializesModels;
private $payment = null;
public function __construct($payment)
{
$this->payment = $payment;
$this->subject($this->payment->subject);
\Log::debug("Subject: {$this->payment->subject}");
}
public function build()
{
$this->to($this->payment->email, $this->payment->name)
->view('mails/payment')
->with('payment', $this->payment);
return $this;
}
}
And I call it this way:
$payment = \App\Models\Payments::findOrFail($id);
$payment->subject = 'Your account has been canceled';
\Mail::queue(new \App\Mail\PlanExpiringOrExpired($payment));
The log saved correctly the following content:
[2023-02-12 11:00:04] local.DEBUG: Subject: Your account has been canceled
Yet the user received as subject: Plan Expiring or Expired (which is basically the class name).
Since I've done this change recently, do you think this might be a cache-related problem? If so, I'm using Supervisor to run queues, how do I clear the cache (through PHP) without messing up the production server?
I have used in the past something like this.
\Artisan::call('cache:clear');
But I'm not sure if this is correct, or if it has any implications for my production server.
Have you tried it this way to setup the proper subject?
private $payment = null;
public function __construct($payment)
{
$this->payment = $payment;
}
public function build()
{
$this->to($this->payment->email, $this->payment->name)
->subject($this->payment->subject)
->view('mails/payment')
->with('payment', $this->payment);
\Log::debug("Subject: {$this->payment->subject}");
return $this;
}
Move the subject set into build
iam doing like this in queue class, EmailContactForm is a mailable class.
public function handle()
{
$email = new EmailContactForm([
'locale' => $this->data['locale'],
'from_email' => $this->data['from_email'],
'name' => $this->data['name'],
'topic' => $this->data['topic'],
'subject' => $this->data['subject'],
'msg' => $this->data['msg']
]);
Mail::to($this->data['to_email'])
->bcc(config('app.mail_from_address'))
->send($email);
}
Solved.
It was indeed a cache problem, it is also necessary to restart the queue. My solution was to create a private endpoint like /superadmin/clear-cache and use it whenever I need.
Route::get('/superadmin/clear-cache', function()
{
\Artisan::call('cache:clear');
\Artisan::call('queue:restart');
});

How to send data from the scheduler to the artisan command in Laravel 5.5

Okay, so I basically need to generate a json file using Laravel's scheduler and a custom artisan command (said list is actually a list of popular cities in our application). So I went ahead and did just that. Here's the definition of my Artisan command:
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Services\PlaceService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Log;
class CitySearch extends Command
{
/**
* The name and signature of the console command.
*
* #var string
*/
protected $signature = 'city:search {--locale=}';
/**
* The console command description.
*
* #var string
*/
protected $description = 'Generates the list of the most popular cities to be used across the application when we need it.';
private $placesService;
/**
* Create a new command instance.
*
* #return void
*/
public function __construct(PlaceService $placesService)
{
$this->placesService = $placesService;
parent::__construct();
}
/**
* Execute the console command.
*
* #return mixed
*/
public function handle()
{
App::setLocale( $this->option('locale') );
$request = Request::create(route('api.search-places'), 'GET', ['maxResults' => 3000, 'isArtisan' => true]);
$request->headers->set('Accept', 'application/json');
$request->headers->set('Api-Key', 'aaaaaaaaaaaa');
// $request->headers->set('Api-Key', '43JSOSH333KSOH555WHO99');
$request->headers->set('App-client', 'web');
$response = app()->handle($request);
$content = json_decode($response->getContent());
$results = array_map(function($element){
if($element->type == "City")
$context = ['an', 'se', 'me'];
else
$context = ['se'];
return ['displayName' => $element->displayName, 'context' => $context];
}, $content->data);
print(json_encode($results));
}
}
Then I went into the scheduler and added the following to have the command run once a week:
namespace App\Console;
use App\Console\Commands\Admin\RedFalcon\PendingTransactionNotificator;
use App\Console\Commands\Admin\RedFalcon\FraudTransactionNotificator;
use App\Console\Commands\CardGenerate;
use App\Console\Commands\Communauto\Stats;
use App\Console\Commands\CPS\Archiver;
use App\Console\Commands\CPS\AutoCasher;
use App\Console\Commands\CPS\Autofixer;
use App\Console\Commands\CPS\Beta\Testers;
use App\Console\Commands\CPS\BNC\EmailFailedBankTransaction;
use App\Console\Commands\CPS\BNC\FeedComposer;
use App\Console\Commands\CPS\BNC\Feeder;
use App\Console\Commands\CPS\BNC\FeedMediator;
use App\Console\Commands\CPS\BNC\FeedReporter;
use App\Console\Commands\CPS\BNC\Parametrizer;
use App\Console\Commands\CPS\Captor;
use App\Console\Commands\CPS\ConfirmationFix;
use App\Console\Commands\CPS\ConfirmationCodeRemoval;
use App\Console\Commands\CPS\DB\RideArchiver;
use App\Console\Commands\CPS\DB\RideFixer;
use App\Console\Commands\CPS\Rider;
use App\Console\Commands\CPS\Test\Resetter;
use App\Console\Commands\CPS\Transactor;
use App\Console\Commands\CPS\Troubleshooting\Experiment1;
use App\Console\Commands\Notifications\ApnFeedbackService;
use App\Console\Commands\RelavelTester;
use App\Console\Commands\UpdateCityPopularity;
use App\Console\Commands\Util\FixBankTransactionTable;
use App\Console\Commands\Util\FixPreauthorizationTable;
use App\Console\Commands\Util\ResetPassword;
use App\Console\Commands\CitySearch;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use Illuminate\Support\Facades\Log;
class Kernel extends ConsoleKernel
{
protected $list;
/**
* The Artisan commands provided by your application.
*
* #var array
*/
protected $commands = [
CardGenerate::class,
Rider::class,
Autofixer::class,
Captor::class,
Transactor::class,
Archiver::class,
FeedComposer::class,
FeedMediator::class,
Feeder::class,
Parametrizer::class,
RideFixer::class,
RideArchiver::class,
RelavelTester::class,
FixPreauthorizationTable::class,
PendingTransactionNotificator::class,
FraudTransactionNotificator::class,
FixBankTransactionTable::class,
Resetter::class,
Testers::class,
Stats::class,
Experiment1::class,
FeedReporter::class,
ResetPassword::class,
AutoCasher::class,
ConfirmationFix::class,
ConfirmationCodeRemoval::class,
CitySearch::class,
UpdateCityPopularity::class,
EmailFailedBankTransaction::class,
ApnFeedbackService::class
];
/**
* Define the application's command schedule.
*
* #param \Illuminate\Console\Scheduling\Schedule $schedule
* #return void
*/
protected function schedule(Schedule $schedule)
{
$schedule->command('city:search --locale=fr')
->mondays()
->at('14:40')
->sendOutputTo(storage_path() . "/app/city-fr.json");
$schedule->command('city:search --locale=en')
->mondays()
->at('14:40')
->sendOutputTo(storage_path() . "/app/city-en.json");
}
/**
* Register the Closure based commands for the application.
*
* #return void
*/
protected function commands()
{
require base_path('routes/console.php');
}
}
Now, this works relatively well... except sometimes it crashes. When that happens, the two json files become filled with error messages instead of the actual data. What I'd like to do is basically save the original list before the command executes and in case something fails, I'd like to output that list into the file and log the error. Right now, everything goes into the file and of course, I get a truckload of errors in my application because the city list is invalid.
Since I'm in Laravel 5.5 I tried using the "before" and "after" hooks (onFailure and onSuccess not available in my version of the framework) and came up with this:
$schedule->command('city:search --locale=fr')
->everyMinute()
->before( function(){
$this->list = json_decode(file_get_contents(storage_path() . "/app/city-fr.json"));
})
->after(function(){
$test = json_decode(file_get_contents(storage_path() . "/app/city-fr.json"));
Log::debug($test);
})
->sendOutputTo(storage_path() . "/app/city-fr.json");
So, while I can successfully get the original list from the file before the process begins, in the "after" hook, the file is still empty so there's no way for me to know whether the process failed or not.
Does anyone know how I should go about this? It feels like the solution is right in front of my face, but I'm just missing it.
Okay, this is a faceplant moment for me. Turns out in the 'after' hook, I did have access to the file, but the reason my output was empty was that the json_decode method returned false because the content of the file wasn't valid json (which was what I was trying to test from the start). Anyway, once I finished picking up the pieces of my scattered brain, the following turned out to work perfectly:
$schedule->command('city:search --locale=fr')
->everyMinute()
->before( function(){
$this->list = file_get_contents(storage_path() . "/app/city-fr.json");
})
->after(function(){
if(!json_decode(file_get_contents(storage_path() . "/app/city-fr.json")))
{
$fp = fopen(storage_path() . "/app/city-fr.json", 'w');
fwrite($fp, $this->list);
fclose($fp);
}
})
->sendOutputTo(storage_path() . "/app/city-fr.json");

How to set metadata and send text (.txt) file as attachment without storing on the server in laravel?

I am working on a module to send chats messages to user email (aka email transcript) using laravel 5.6.
I need to save all the chat messages to a txt file and send that file as attachment to user's email address.
I do not want to save the txt file to my server as many people would be using that application and it will increase the storage usage of the server i.e I need to generate the txt file in memory.
I am able to populate the chats in plain email without attachment but this is not the solution if the chat messages increase, email would be too lengthy and seems not professional to me.
This I have tried so far:
EmailTranscriptController.php
<?php
namespace App\Http\Controllers\Home;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Trade;
use App\Models\ChatMessage;
use Auth;
use Illuminate\Support\Facades\Mail;
use App\Mail\EmailTradeChatMessages;
use Validator;
class EmailTranscriptController extends Controller
{
public function emailTradeTranscript($tradeId)
{
$userId = Auth::id();
$userEmail = Auth::user()->email;
$trade = Trade::findClosedTradeByIdByUserId($tradeId, $userId);
if (is_null($trade)) {
return response()->api(false, 'Trade not available', null);
}
$tradeStartTime = $trade->created_at;
$tradeCloseTime = $trade->updated_at;
$tradeChats = ChatMessage::getAllChatByTradeId($tradeId);
Mail::to($userEmail)->queue(new EmailTradeChatMessages(
$tradeChats,
$tradeStartTime,
$tradeCloseTime
));
return response()->api(true, 'Email Sent Successfully', null);
}
}
EmailTradeChatMessages.php
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue;
class EmailTradeChatMessages extends Mailable
{
use Queueable, SerializesModels;
protected $chats;
protected $tradeStartTime;
protected $tradeCloseTime;
/**
* Create a new message instance.
*
* #return void
*/
public function __construct($chats, $tradeStartTime, $tradeCloseTime)
{
$this->chats = $chats;
$this->tradeStartTime = $tradeStartTime;
$this->tradeCloseTime = $tradeCloseTime;
}
/**
* Build the message.
*
* #return $this
*/
public function build()
{
return $this->markdown('emails.trade_chat_transcript')->with([
'chats' => $this->chats,
'tradeStartTime' => $this->tradeStartTime,
'tradeCloseTime' => $this->tradeCloseTime,
]);
}
}
trade_chat_transcript.blade.php (dummy)
#component('mail::message')
#Trade Started at: {{$tradeStartTime}}
#php
$count=0;
#endphp
#foreach($chats as $chat)
{{++$count}}
#endforeach
#Trade Closed at: {{$tradeCloseTime}}
Thanks,<br>
{{ config('app.name') }}
#endcomponent
Kindly help me getting the solution, I would also like to get other approaches to solution,if any.
Update
I found the solution for not storing the file on server itself and attach it using attachData() method, as follows:
public function build()
{
$email= $this->markdown('emails.trade_chat_transcript')->with([
'tradeId' => $this->tradeId,
'filename' => $this->filename,
'tradeStartTime' => $this->tradeStartTime,
'tradeCloseTime' => $this->tradeCloseTime,
])
->attachData($this->message,$this->filename,[
'mime'=>'text/plain'
]);
return $email;
}
Now I need to set metadata of the file to be attached in email eg. Author etc.
You need to create the file on the server. That being said you can delete it directly after. There is a method for that:
return response()->download($pathToFile)->deleteFileAfterSend(true);

Trouble dispatching Laravel Queue

I am currently trying to send mail using a queue in Laravel 5.4 in order to speed up a few requests. But for some reason I just won't resolve.
My job looks like the following:
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Support\Facades\Mail;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
class NotificationEmail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $doer, $does, $user;
/**
* Create a new job instance.
*
* #param Podcast $podcast
* #return void
*/
public function __construct($doer, $does, $user)
{
$this->doer = $doer;
$this->does = $does;
$this->user = $user;
}
/**
* Execute the job.
*
* #param AudioProcessor $processor
* #return void
*/
public function handle()
{
$actions = [
'accepted.invite' => 'accepted your invited.',
'accepted.requesting' => 'accepted your request.',
'denied.invite' => 'denied your invite.',
'denied.requesting' => 'denied your request'
];
Mail::send('emails.notification', [
'doer' => $this->does,
'action' => $actions[$this->action]
], function ($m) {
$m->from('noreply#bigriss.com', 'Bigriss');
$m->to("myemail#gmail.com", 'Shawn')->subject('New Notification');
echo "SENT";
});
}
}
With it being dispatched in another class by:
NotificationEmail::dispatch($doer, $does, $user);
Upon listening to the queue, php artisan queue:listen, as soon as I dispatch the job, the listener just runs on endlessly trying to resolve the handle function. I am getting the message "SENT" but the email is never sent (as I can see on my email provider) and the queue is never actually remove instead, the attempts count just goes up indefinitely. Am I missing something here? Is this not what queues are good for?
You are passing string into your to function, and you're missing a variable in your closure.
When you have an anonymous function, you need to pass in any extra variables using use. I don't see a $user variable anywhere in your handle method. It will need to be passed in as a separate variable because you cannot use $this->user to pass it into the closure.
Right now you have
$m->to("$user->email", 'Shawn')->subject('New Notification');
Which is literally interpreting that as a string that says $user->email because you haven't passed anything in. (Side note: there's really no reason to use that here, save that for inline variables with file paths, etc. You don't need an inline variable with this string).
You would need to change it to
$user = $this->user;
Mail::send('emails.notification', [
'doer' => $this->does,
'action' => $actions[$this->action]
], function ($m) use ($user) {
$m->from('noreply#bigriss.com', 'Bigriss');
$m->to($user->email, 'Shawn')->subject('New Notification');
echo "SENT";
});
You may want to consider using something like Laravel Dusk to debug your queue and logging to better control this than trying to just view "SENT" in your browser.
Also, consider sanitizing your website address since you're posting source code from it.

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