Testing booted method in Laravel - laravel

I am using Laravel Cashier with Stripe, and I am trying to cover the the following lines in my tests, so far I tried mocking but that did not work, I was wondering what is the best practice to test them
use function Illuminate\Events\queueable;
/**
* The "booted" method of the model.
*
* #return void
*/
protected static function booted()
{
static::updated(queueable(function ($customer) {
if ($customer->hasStripeId()) {
$customer->syncStripeCustomerDetails();
}
}));
}
The code is taken from https://laravel.com/docs/9.x/billing#syncing-customer-data-with-stripe

You could achieve this by using a partial mock, Laravel has a helper for that method. Since you are actually using correct dependency injection, this would be my approach. It requires the queue setting in phpunit to be sync.
public function test_update_customer()
{
$account = Account::factory()->create(['stripe_id' => 'fake']);
$this->partialMock(Account::class, function ($mock) {
$mock->shouldReceive('syncStripeCustomerDetails')
->once();
});
$response = $this->actingAs($someUser)
->put(route('billing.update', $account), [
'name' => 'Some name',
'email' => 'Another email',
]);
$response->assertOk();
}

Related

Mocking Dependency Injected URL parameter in PHPUnit

I am trying to create a test for a feature I've written.
The logic is quite simple:
From the api.php I am calling the store method:
Route::group(['prefix' => '/study/{study}/bookmark_list'], function () {
...
Route::post('/{bookmarkList}/bookmark', 'BookmarkController#store');
...
});
thus I am injecting the study and the bookmark list.
My controller passes down the parameters
public function store(Study $study, BookmarkList $bookmarkList)
{
return $this->serve(CreateBookmarkFeature::class);
}
And I am using them in the Feature accordingly
'bookmark_list_id' => $request->bookmarkList->id,
class CreateBookmarkFeature extends Feature
{
public function handle(CreateBookmarkRequest $request)
{
//Call the appropriate job
$bookmark = $this->run(CreateBookmarkJob::class, [
'bookmark_list_id' => $request->bookmarkList->id,
'item_id' => $request->input('item_id'),
'type' => $request->input('type'),
'latest_update' => $request->input('latest_update'),
'notes' => $request->input('notes')
]);
//Return
return $this->run(RespondWithJsonJob::class, [
'data' => [
'bookmark' => $bookmark
]
]);
}
}
I am also using a custom request (CreateBookmarkRequest) which practically verifies if the user is authorised and imposes some rules on the input.
class CreateBookmarkRequest extends JsonRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return $this->getAuthorizedUser()->canAccessStudy($this->study->id);
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
"item_id" => ["integer", "required"],
"type" => [Rule::in(BookmarkType::getValues()), "required"],
"latest_update" => ['date_format:Y-m-d H:i:s', 'nullable'],
"text" => ["string", "nullable"]
];
}
}
Now, here comes the problem. I want to write a test for the feature that tests that the correct response is being returned (it would be good to verify the CreateBookmarkJob is called but not that important). The problem is that although I can mock the request, along with the input() method, I cannot mock the injected bookmarkList.
The rest of the functions are mocked properly and work as expected.
My test:
class CreateBookmarkFeatureTest extends TestCase
{
use WithoutMiddleware;
use DatabaseMigrations;
public function setUp(): void
{
parent::setUp();
// seed the database
$this->seed();
}
public function test_createbookmarkfeature()
{
//GIVEN
$mockRequest = $this->mock(CreateBookmarkRequest::class);
$mockRequest->shouldReceive('authorize')->once()->andReturnTrue();
$mockRequest->shouldReceive('rules')->once()->andReturnTrue();
$mockRequest->shouldReceive('input')->once()->with('item_id')->andReturn(1);
$mockRequest->shouldReceive('input')->once()->with('type')->andReturn("ADVOCATE");
$mockRequest->shouldReceive('input')->once()->with('latest_update')->andReturn(Carbon::now());
$mockRequest->shouldReceive('input')->once()->with('notes')->andReturn("acs");
$mockRequest->shouldReceive('bookmark_list->id')->once()->andReturn(1);
//WHEN
$response = $this->postJson('/api/recruitment_toolkit/study/1/bookmark_list/1/bookmark', [
"type"=> "ADVOCATE",
"item_id"=> "12",
"text"=> "My first bookmark"
]);
//THEN
$this->assertEquals("foo", $response['data'], "das");
}
One potential solution that I though would be to not mock the request, but this way I cannot find a way to mock the "returnAuthorisedUser" in the request.
Any ideas on how to mock the injected model would be appreciated, or otherwise any idea on how to properly test the feature in case I am approaching it wrong.
It is worth mentioning that I have separate unit tests for each of the jobs (CreateBookmarkJob and RespondWithJSONJob).
Thanks in advance
A feature test, by definition, will be imitating an end-user action. There's no need to mock the request class, you just make the request as a user would.
Assuming a Study with ID 1 and a BookmarkList with ID 1 have been created by your seeder, Laravel will inject appropriate dependencies via route model binding. If not, you should use a factory method to create models and then substitute the appropriate ID in the URL.
<?php
namespace Tests\Feature;
use Tests\TestCase;
class CreateBookmarkFeatureTest extends TestCase
{
use WithoutMiddleware;
use DatabaseMigrations;
public function setUp(): void
{
parent::setUp();
$this->seed();
}
public function TestCreateBookmarkFeature()
{
$url = '/api/recruitment_toolkit/study/1/bookmark_list/1/bookmark';
$data = [
"type"=> "ADVOCATE",
"item_id"=> "12",
"text"=> "My first bookmark"
];
$this->postJson($url, $data)
->assertStatus(200)
->assertJsonPath("some.path", "some expected value");
}
}
I agree with #miken32's response - that a feature should indeed imitate a user interaction - however the dependency injection via route model binding still did not work.
After spending some hours on it, I realised that the reason for it is that
use WithoutMiddleware;
disables all middleware, even the one responsible for route model binding, therefore the object models were not injected in the request.
The actual solution for this is that (for laravel >=7) we can define the middleware we want to disable, in this case:
$this->withoutMiddleware(\App\Http\Middleware\Authenticate::class);
Then we just use
$user = User::where('id',1)->first(); $this->actingAs($user);
And everything else works as expected.
DISCLAIMER: I am not implying that miken32's response was incorrect; it was definitely in the right direction - just adding this as a small detail.

Store json array in the api controller -laravel 8

I'm a little bit confused to how I can store data in my api controller,
My json looks like this:
[
{ a: 1 },
{ a: 2 }
]
I have my rules
$rules = [
'*.a' => 'required',
];
I have my validation
$validator = Validator::make($request->all(), $rules);
if ($validator->fails()) {
$error = $validator->messages()->toJson();
return response($error, 200);
}
and now there's my "problem": I would like to make a cleaner code.
My old option was pass the request->all() to a variable , json decode the contenent and make a foreach cycle to store data as here:
foreach ($datas as $data) {
$data = new rawData([
'a' => $data->a,
]);
$newrawData->save();
}
can I do a cleaner thing?? and How?
You can put your validation logic in a custom form request validator.
First, create the validator
php artisan make:request ExampleRequest
You can find the newly created a new class in app/Http/Requests/ExampleRequest.php and you can add your rules as follows
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class ExampleRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'*.a' => 'required',
];
}
}
Now your controller method/action will not be executed unless the request passes the validation rules. you can use it in your controller method as follows:
In app/Http/Controllers/ExampleController.php
public function store(ExampleRequest $request)
{
// Your normal code.
}
that looks quite good. There is only one small thing I would have done differently. But it's a matter of taste.
foreach ($datas as $data) {
rawData::create($data->toArray());
}
And you can you use the request object directly to validate. or you implement an custom Request Object what you pass as parameter in your function.
$request->validate([
'*.a' => 'required',
]);

Why laravel 8 form validation request doesn't work as shown in documentation

I make custom request by
php artisan make:request UserUpdate
and then fill UserUpdate
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'name'=>'required|string',
'email'=>'required|email',
];
}
after that call from controller
public function profilepost(UserUpdate $request)
{
$user = Auth::user();
$user->name = $request['name'];
$user->email = $request['email'];
$user->save();
return back();
}
when submit form show error
Target class [app\Http\Requests\UserUpdate] does not exist.
Why that happen? Laravel documentation are not correct? Can someone explain me?
I Know it's solved, but this could be useful!
It's good practice to name the file/class like: UpdateUserRequest or UserUpdateRequest.
Why? It's more readable, other programmers understand what it does faster and
because of the specific name, class name collitions are avoided.
Remember if you have multiple requests on you project it's allways better to organize them in folders, example:
php artisan make:request User/UserUpdateRequest
This should create a request in User folder, and also with: namespace App\Http\Requests\User
Last but not least a better way to define the rules is:
public function rules()
{
return [
'name' => ['required', 'string'],
'email' => ['required', 'email'],
];
}
Why? This gives you the ability to add custom rules to the validations if needed!

Call to undefined method Illuminate\Database\Eloquent\Relations\BelongsToMany::routeNotificationFor()

I'm building a messaging system that notifies each user in the conversation when a reply is set.
MessageNotification.php
class MessageNotification extends Notification
{
use Queueable;
/**
* Get the notification's delivery channels.
*
* #param mixed $notifiable
* #return array
*/
public function via($notifiable)
{
return ['database'];
}
public function toArray($notifiable)
{
return [
'data' => 'Messenger notification'
];
}
}
InboxController
public function reply($hashedId, Request $request)
{
$this->validate($request, [
'body' => 'required',
]);
$conversation = Conversation::where('hashed_id', $hashedId)->first();
$users = $conversation->participants();
//dd($conversationUserIds);
$notifications = Notification::send($users, new MessageNotification());
$message = $conversation->messages()->create([
'sender_id' => auth()->user()->id,
'body' => $request->body,
]);
return new MessageResource($message);
}
Error
Call to undefined method Illuminate\Database\Eloquent\Relations\BelongsToMany::routeNotificationFor()
Extra Information
I've had to build a custom Notifiable trait due to needing to use both Laravel Sparks notification system and Laravels stock notification system. Tutorial I got code from.
Custom notification trait
namespace App\Traits;
use Illuminate\Notifications\Notifiable as BaseNotifiable;
use App\Notifications\DatabaseNotification;
trait Notifiable {
use BaseNotifiable;
public function notifications() {
return $this->morphMany(DatabaseNotification::class, 'notifiable')->orderBy('created_at', 'desc');
}
}
Also note that $reciever->notify(new MessageNotification()); works just fine when sending a notification to one user. The only other solution I saw on this was: https://laracasts.com/discuss/channels/code-review/call-to-undefined-method-routenotificationfor-while-sending-email-to-multiple-users
I tried to implement that, but I'm using a database channel so it shouldn't make a difference.
This line here:
$users = $conversation->participants();
Will set the $users variable to a QueryBuilder instance (assuming you are using conventional Laravel relationships), rather than a collection of users. This is because the () at the end of a relationship builds the query but doesn't run it yet. So then when you call Notification::send($users, etc...) you are not passing in a collection of users; you are passing in a QueryBuilder object.
Try this instead:
$users = $conversation->participants;
Again - this is assuming that the participants method on the Conversation model is a standard laravel relationship.

how to add extra data into create method with laravel

public function store(Request $request) {
$user = Book::create([
'user_id' => auth()->id(),
'name => $request->name,
'year => $request->year
)];
}
The above code is able to store into Database.
I want to know how to add below extra data TOGETHER.
I found out that merge was not working as it is not collection.
Tried to chain but was not working.
public function data() {
$array = [
'amount' => 30,
'source' => 'abcdef',
];
return $array;
}
You can catch create Event in Model.
This code may can help you.
/**
* to set default value in database when new record insert
*
*/
public static function bootSystemTrait()
{
static::creating(function ($model) {
$model->amount = 30;
});
}
You can write this code into your model. It will execute every time when you create record using Laravel model.
If you want to modify it you can use property into Model class. Something like this:
class TestClass extends Model
{
public static $amount = 0;
static::creating(function ($model) {
$model->amount = self::$amount;
});
}
Good Luck.

Resources